From a2f8e71e2b9cb1df518be321ef149ff2daa6f4d5 Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Tue, 1 Jun 2021 21:37:08 +0200 Subject: [PATCH] changed Contour Scanner to Contour Scanner and Trimmer --- .../contourscanner/contour_scanner.inx | 79 -- .../contourscanner/contour_scanner.py | 295 -------- .../contour_scanner_and_trimmer.inx | 125 ++++ .../contour_scanner_and_trimmer.py | 701 ++++++++++++++++++ .../poly_point_isect.py | 0 5 files changed, 826 insertions(+), 374 deletions(-) delete mode 100644 extensions/fablabchemnitz/contourscanner/contour_scanner.inx delete mode 100644 extensions/fablabchemnitz/contourscanner/contour_scanner.py create mode 100644 extensions/fablabchemnitz/contourscannerandtrimmer/contour_scanner_and_trimmer.inx create mode 100644 extensions/fablabchemnitz/contourscannerandtrimmer/contour_scanner_and_trimmer.py rename extensions/fablabchemnitz/{contourscanner => contourscannerandtrimmer}/poly_point_isect.py (100%) diff --git a/extensions/fablabchemnitz/contourscanner/contour_scanner.inx b/extensions/fablabchemnitz/contourscanner/contour_scanner.inx deleted file mode 100644 index 616962f8..00000000 --- a/extensions/fablabchemnitz/contourscanner/contour_scanner.inx +++ /dev/null @@ -1,79 +0,0 @@ - - - Contour Scanner - fablabchemnitz.de.contour_scanner - - - - - - true - 4012452351 - true - 2330080511 - true - 1923076095 - true - 4239343359 - 10 - true - true - - - - - false - false - false - - - false - false - 1.0 - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ../000_about_fablabchemnitz.svg - - - - path - - - - - - - - \ No newline at end of file diff --git a/extensions/fablabchemnitz/contourscanner/contour_scanner.py b/extensions/fablabchemnitz/contourscanner/contour_scanner.py deleted file mode 100644 index f39c9793..00000000 --- a/extensions/fablabchemnitz/contourscanner/contour_scanner.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/extensions/fablabchemnitz/contourscannerandtrimmer/contour_scanner_and_trimmer.inx b/extensions/fablabchemnitz/contourscannerandtrimmer/contour_scanner_and_trimmer.inx new file mode 100644 index 00000000..7d644c5b --- /dev/null +++ b/extensions/fablabchemnitz/contourscannerandtrimmer/contour_scanner_and_trimmer.inx @@ -0,0 +1,125 @@ + + + Contour Scanner And Trimmer + fablabchemnitz.de.contour_scanner_and_trimmer + + + + false + false + false + true + 0.100 + 3 + 0.1 + false + + + + false + false + false + false + false + false + false + false + + + + + + + + + + false + true + true + false + true + false + + + + + + + + 1.0 + 30 + false + true + + 4012452351 + 2330080511 + 2593756927 + 1630897151 + 6320383 + 4239343359 + + + + + 3227634687 + 1923076095 + 3045284607 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + all + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/contourscannerandtrimmer/contour_scanner_and_trimmer.py b/extensions/fablabchemnitz/contourscannerandtrimmer/contour_scanner_and_trimmer.py new file mode 100644 index 00000000..6003c436 --- /dev/null +++ b/extensions/fablabchemnitz/contourscannerandtrimmer/contour_scanner_and_trimmer.py @@ -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 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() diff --git a/extensions/fablabchemnitz/contourscanner/poly_point_isect.py b/extensions/fablabchemnitz/contourscannerandtrimmer/poly_point_isect.py similarity index 100% rename from extensions/fablabchemnitz/contourscanner/poly_point_isect.py rename to extensions/fablabchemnitz/contourscannerandtrimmer/poly_point_isect.py