From 11d09b7f921133932c674d6017a3276f6bb9a432 Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Wed, 19 May 2021 02:14:15 +0200 Subject: [PATCH] fixes in unwind paths; added mpdify path start extension --- .../modify_path_start/modify_path_start.inx | 55 ++++ .../modify_path_start/modify_path_start.py | 177 +++++++++++++ .../path_number_subpaths.py | 22 +- .../unwind_paths/unwind_paths.inx | 2 +- .../unwind_paths/unwind_paths.py | 234 +++++++++--------- 5 files changed, 362 insertions(+), 128 deletions(-) create mode 100644 extensions/fablabchemnitz/modify_path_start/modify_path_start.inx create mode 100644 extensions/fablabchemnitz/modify_path_start/modify_path_start.py diff --git a/extensions/fablabchemnitz/modify_path_start/modify_path_start.inx b/extensions/fablabchemnitz/modify_path_start/modify_path_start.inx new file mode 100644 index 00000000..cea5718a --- /dev/null +++ b/extensions/fablabchemnitz/modify_path_start/modify_path_start.inx @@ -0,0 +1,55 @@ + + + Modify Path Start Node + fablabchemnitz.de.modify_path_start + + + true + 0 + false + 10px + 10px + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + + + + path + + + + + + + + \ No newline at end of file diff --git a/extensions/fablabchemnitz/modify_path_start/modify_path_start.py b/extensions/fablabchemnitz/modify_path_start/modify_path_start.py new file mode 100644 index 00000000..4f076e10 --- /dev/null +++ b/extensions/fablabchemnitz/modify_path_start/modify_path_start.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 + +''' + +Author: Mario Voigt / FabLab Chemnitz +Mail: mario.voigt@stadtfabrikanten.org +Date: 19.05.2021 +Last patch: 19.05.2021 +License: GNU GPL v3 + +ToDo: +add group id like path id +link for usage: plotting/laser cutting, path unwinding plugin > startpunkt für anwicklung +''' + +import copy +import inkex +from inkex import Circle, TextElement, Path, PathElement, CubicSuperPath + +class ModifyStartDirection(inkex.EffectExtension): + + def modify(self, element): + 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:]) + if self.options.debug is True: + self.msg("Element {} has {} subpath(s)".format(element.get('id'), len(subpaths))) + + subpathNr = 0 + for path in subpaths: + subpathNr += 1 + newSubpaths = subpaths #we will overwrite them later + + pathIsClosed = False + if path[-1][0] == 'Z' or \ + (path[-1][0] == 'L' and path[0][1] == path[-1][1]) or \ + (path[-1][0] == 'C' and path[0][1] == [path[-1][1][-2], path[-1][1][-1]]) \ + : #if first is last point the path is also closed. The "Z" command is not required + pathIsClosed = True + + if self.options.debug is True: + self.msg("pathIsClosed = " + str(pathIsClosed)) + + if self.options.closed_only is True and pathIsClosed is False: + self.msg("Path {}/subpath {} is not closed!".format(element.get('id'), subpathNr)) + continue #skip this open path + + if path[-1][0] == 'Z': #replace Z with another L command (which moves to the coordinates of the first M command in path) to have better overview + path[-1][0] = 'L' + path[-1][1] = path[0][1] + + #adjust if entered move number is higher than actual node count. We handle as infinite looping + moves = self.options.movenode % len(path) + if pathIsClosed is True: #if closed start and end collapse and "duplicate" + moves = self.options.movenode % (len(path) - 1) + + if self.options.debug is True: + self.msg("root path:") + self.msg(path) + self.msg("-"*25) + + if self.options.debug is True: + self.msg("moves = " + str(moves)) + + for i in range(moves): + if len(path) > 2: #the path needs at least more than two nodes + #we move the first node to the end of the list + move = path[0] + del path[0] + path.append(move) + + if self.options.debug is True: + self.msg("moved path:") + self.msg(path) + self.msg("-"*25) + + oldseg = copy.deepcopy(path[0]) #if we assign like "oldseg = path[0]", it will get overwritten. So we need copy + + #Now we messed the integrity of the path. It does not begin with 'M' now. But we need an 'M'. + #It now either starts with L or C. H, V, Z cannot occure here. + if path[0][0] == 'C': #and path[-1][0] == 'M': + #self.msg("C to M") + path[0][1] = [path[0][1][-2], path[0][1][-1]] + elif path[0][0] == 'L': #and path[-1][0] == 'M': + #self.msg("L to M") + path[0][1] = [path[0][1][0], path[0][1][1]] + #else: + # self.msg("no idea") + path[0][0] = 'M' #we really need M. Does not matter if 'L' or 'C'. + + if pathIsClosed is True: + if path[-1][0] == 'M' and len(oldseg[1]) == 2: #data of an 'L' command + path[-1][0] = 'L' + path[-1][1] = path[0][1] + elif path[-1][0] == 'M' and len(oldseg[1]) > 2: #data of an 'C' command + path[-1][0] = 'C' + path[-1][1] = oldseg[1] + else: + if path[-1][0] == 'M': #if open path we just drop the dangling 'M' command completely + del path[-1] + + if self.options.debug is True: + self.msg("final path:") + self.msg(path) + self.msg("-"*25) + + newSubpaths[subpathNr - 1] = path + #else: + # inkex.utils.debug("More moves entered than possible to apply") + + composedPath = inkex.Path() + for newSubpath in newSubpaths: + composedPath.extend(newSubpath) + + if self.options.debug is True: + self.msg("Composed path = " + str(composedPath)) + + element.path = composedPath + + def visualizeFirstTwo(self, element): + """Add a dot label for this path element""" + group = element.getparent().add(inkex.Group()) + dot_group = group.add(inkex.Group(id="dot-group-" + element.get('id'))) + num_group = group.add(inkex.Group(id="num-group-" + element.get('id'))) + group.transform = element.transform + radius = self.svg.unittouu(self.options.dotsize) / 2 + + count = 0 + for step, (x, y) in enumerate(element.path.end_points): + count += 1 + circle = Circle(cx=str(x), cy=str(y), r=str(radius)) + circle.style = inkex.Style({'stroke': 'none', 'fill': '#000'}) + + text = TextElement(x=str(x + radius), y=str(y - radius)) + text.text = str(count) #we start with #1 + text.style = inkex.Style({'font-size': self.svg.unittouu(self.options.fontsize), 'fill-opacity': '1.0', 'stroke': 'none', + 'font-weight': 'normal', 'font-style': 'normal', 'fill': '#999'}) + + dot_group.append(circle) + num_group.append(text) + + if count > 1: #we only display first two points to see the position of the first node and the path direction + break + + def add_arguments(self, pars): + pars.add_argument("--tab") + pars.add_argument("--closed_only", type=inkex.Boolean, default=False, help="If disabled we also apply on open (sub)path. Warning: This REMOVES segments!") + pars.add_argument("--movenode", type=int, default=0, help="Move starting node n nodes further") + pars.add_argument('--visualize_result', type=inkex.Boolean, default=False, help="If enabled each node gets a number and a dot") + pars.add_argument("--dotsize", default="10px", help="Size of the dots on the path nodes") + pars.add_argument("--fontsize", default="20px", help="Size of node labels") + pars.add_argument("--debug", type=inkex.Boolean, default=False, help="Debug Output") + + def effect(self): + if len(self.svg.selected) > 0: + elements = self.svg.selection.filter(PathElement).values() + if len(elements) > 0: + for element in elements: + #move starting element / change direction + self.modify(element) + + #finally apply dots to visualize the result + if self.options.visualize_result is True: + self.visualizeFirstTwo(element) + else: + inkex.errormsg('Selection seems not to contain path elements. Maybe you have selected a group instead?') + return + else: + inkex.errormsg('Please select some objects first.') + return + +if __name__ == '__main__': + ModifyStartDirection().run() \ No newline at end of file diff --git a/extensions/fablabchemnitz/path_number_subpaths/path_number_subpaths.py b/extensions/fablabchemnitz/path_number_subpaths/path_number_subpaths.py index 8ec8d510..41997145 100644 --- a/extensions/fablabchemnitz/path_number_subpaths/path_number_subpaths.py +++ b/extensions/fablabchemnitz/path_number_subpaths/path_number_subpaths.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # # Copyright (C) 2005 Aaron Spike, aaron@ekips.org -# Modified by Ellen Wasbo, ellen@wasbo.net 2021 - number subpaths and mark start/end node with green/red dot +# Modified by Ellen Wasbo, ellen@wasbo.net 2021 - number subpaths and mark start/end element with green/red dot # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,34 +22,34 @@ import inkex from inkex import TextElement, Circle class NumberSubpaths(inkex.EffectExtension): - """Mark start and end nodes with numbered dots according to the options""" + """Mark start and end elements with numbered dots according to the options""" def add_arguments(self, pars): - pars.add_argument("--dotsize", default="10px", help="Size of the dots on the path nodes") - pars.add_argument("--fontsize", default="10px", help="Size of node labels") + pars.add_argument("--dotsize", default="10px", help="Size of the dots on the path elements") + pars.add_argument("--fontsize", default="10px", help="Size of element labels") pars.add_argument("--showID", type=inkex.Boolean, default=False) def effect(self): if not self.svg.selected: raise inkex.AbortExtension("Please select an object.") - for id, node in self.svg.selection.id_dict().items(): - self.add_dot(node) + for element in self.svg.selection.values(): + self.add_dot(element) - def add_dot(self, node): + def add_dot(self, element): """Add a dot label for this path element""" - group = node.getparent().add(inkex.Group()) + group = element.getparent().add(inkex.Group()) dot_group = group.add(inkex.Group()) num_group = group.add(inkex.Group()) - group.transform = node.transform + group.transform = element.transform styleStart = inkex.Style({'stroke': 'none', 'fill': '#00ff00'}) styleEnd = inkex.Style({'stroke': 'none', 'fill': '#ff0000'}) idTxt='' if self.options.showID==True: - idTxt=node.get('id')+', ' + idTxt=element.get('id')+', ' cc=0 - for sub in node.path.to_superpath(): + for sub in element.path.to_superpath(): x=sub[0][1][0] y=sub[0][1][1] circle = dot_group.add(Circle(cx=str(x), cy=str(y), r=str(self.svg.unittouu(self.options.dotsize) / 2))) diff --git a/extensions/fablabchemnitz/unwind_paths/unwind_paths.inx b/extensions/fablabchemnitz/unwind_paths/unwind_paths.inx index 48f36653..1acb658d 100644 --- a/extensions/fablabchemnitz/unwind_paths/unwind_paths.inx +++ b/extensions/fablabchemnitz/unwind_paths/unwind_paths.inx @@ -3,7 +3,7 @@ Unwind Paths fablabchemnitz.de.unwind_paths - + false false diff --git a/extensions/fablabchemnitz/unwind_paths/unwind_paths.py b/extensions/fablabchemnitz/unwind_paths/unwind_paths.py index e7df8588..1f9395ac 100644 --- a/extensions/fablabchemnitz/unwind_paths/unwind_paths.py +++ b/extensions/fablabchemnitz/unwind_paths/unwind_paths.py @@ -40,7 +40,7 @@ class UnwindPaths(inkex.EffectExtension): def add_arguments(self, pars): pars.add_argument('--tab') pars.add_argument('--keep_original', type=inkex.Boolean, default=False, help="If selected, the original paths get deleted") - pars.add_argument('--break_apart', type=inkex.Boolean, default=False) + pars.add_argument('--break_apart', type=inkex.Boolean, default=False, help="Split each path into single curve segments") pars.add_argument('--colorize', type=inkex.Boolean, default=False, help="Requires enabled 'Break apart' option") pars.add_argument('--extrude', type=inkex.Boolean, default=False) pars.add_argument('--extrude_height', type=float, default=10.000) @@ -88,126 +88,128 @@ class UnwindPaths(inkex.EffectExtension): if len(self.svg.selected) > 0: #we break apart combined paths to get distinct contours + breakApartPaths = [] for element in self.svg.selection.filter(inkex.PathElement).values(): - breakApartPaths = self.breakContours(element) + breakApartPaths.append(self.breakContours(element)) - for element in breakApartPaths: - elemGroup = self.svg.get_current_layer().add(inkex.Group(id="unwinding-" + element.get('id'))) - - #beginning point of the unwind band: - bbox = element.bounding_box() #shift the element to the bottom of the element - xmin = bbox.left - ymax = bbox.bottom + bbox.height * 0.1 #10% additional spacing - - csp = element.path.to_superpath() - subCount = len(element.path) - - #generate random colors; used to identify glue tab pairs - if self.options.colorize is True: - randomColorSet = [] - while len(randomColorSet) < subCount - 1: - r = lambda: random.randint(0,255) - newColor = '#%02X%02X%02X' % (r(),r(),r()) - if newColor not in randomColorSet: - randomColorSet.append(newColor) - - for sub in csp: - #generate new horizontal line data by measuring each segment - new = [] - new.append([sub[0]]) - i = 1 - topPathData = "m {:0.6f},{:0.6f} ".format(xmin, ymax) - bottomPathData = "m {:0.6f},{:0.6f} ".format(xmin, ymax + shifting) - lengths = [] - - if self.options.break_apart is True: - topLineGroup = self.svg.get_current_layer().add(inkex.Group(id="hline-top-" + element.get('id'))) - bottomLineGroup = self.svg.get_current_layer().add(inkex.Group(id="hline-bottom-" + element.get('id'))) - newOriginalPathGroup = self.svg.get_current_layer().add(inkex.Group(id="new-original-" + element.get('id'))) - elemGroup.append(topLineGroup) - elemGroup.append(bottomLineGroup) - self.svg.get_current_layer().append(newOriginalPathGroup) #we want this to be one level above unwound stuff - - if self.options.extrude is True: - vlinesGroup = self.svg.get_current_layer().add(inkex.Group(id="vlines-" + element.get('id'))) - elemGroup.append(vlinesGroup) - - while i <= len(sub) - 1: - stroke_color = '#000000' - if self.options.colorize is True and self.options.break_apart is True: - stroke_color =randomColorSet[i-1] - - horizontal_line_style = {'stroke':stroke_color,'stroke-width':'1px','fill':'none'} - - length = bezier.cspseglength(new[-1][-1], sub[i]) #sub path length - segment = "h {:0.6f} ".format(length) - topPathData += segment - bottomPathData += segment - new[-1].append(sub[i]) #important line! - if self.options.break_apart is True: - self.drawline("m {:0.6f},{:0.0f} ".format(xmin + sum([length for length in lengths]), ymax) + segment, - "segmented-top-{}-{}".format(element.get('id'), i), topLineGroup, horizontal_line_style) - if self.options.extrude is True: - self.drawline("m {:0.6f},{:0.0f} ".format(xmin + sum([length for length in lengths]), ymax + shifting) + segment, - "segmented-bottom-{}-{}".format(element.get('id'), i), bottomLineGroup, horizontal_line_style) - lengths.append(length) - i += 1 - - - if self.options.break_apart is False: - self.drawline(topPathData, "combined-top-{0}".format(element.get('id')), elemGroup, horizontal_line_style) - if self.options.extrude is True: - self.drawline(bottomPathData, "combined-bottom-{0}".format(element.get('id')), elemGroup, horizontal_line_style) - - #draw as much vertical lines as segments in bezier + start + end vertical line - vertical_end_lines_style = {'stroke':'#000000','stroke-width':'1px','fill':'none'} - if self.options.extrude is True: - #render start line - self.drawline("m {:0.6f},{:0.6f} v {:0.6f}".format(xmin, ymax, shifting),"vline-{}-start".format(element.get('id')), vlinesGroup, vertical_end_lines_style) - #render divider lines - if self.options.render_vertical_dividers is True: - vertical_mid_lines_style = {'stroke':'#000000','stroke-width':'1px','fill':'none'} - if self.options.render_with_dashes is True: - vertical_mid_lines_style = {'stroke':'#000000','stroke-width':'1px',"stroke-dasharray":"2 2", 'fill':'none'} - x = 0 - for n in range(0, i-2): - x += lengths[n] - self.drawline("m {:0.6f},{:0.6f} v {:0.6f}".format(xmin + x, ymax, shifting),"vline-{}-{}".format(element.get('id'), n + 1), vlinesGroup, vertical_mid_lines_style) - #render end line - self.drawline("m {:0.6f},{:0.6f} v {:0.6f}".format(xmin + sum([length for length in lengths]), ymax, shifting),"vline-{}-end".format(element.get('id')), vlinesGroup, vertical_end_lines_style) - - if self.options.break_apart is True: - # Split (already broken apart) paths into detached segments - raw = Path(element.get("d")).to_arrays() #returns Uppercase Command Letters; does not include H, V - for i in range(len(raw)): - if i > 0: - - if raw[i-1][0] in ("M", "L"): - startPoint = "M {},{}".format(raw[i-1][1][0], raw[i-1][1][1]) - elif raw[i-1][0] == 'C': - startPoint = "M {},{}".format(raw[i-1][1][-2], raw[i-1][1][-1]) - else: - inkex.utils.debug("Start point error. Unknown command!") - - if raw[i][0] in ("M", "L"): - segment = " {},{}".format(raw[i][1][0], raw[i][1][1]) - elif raw[i][0] == 'C': - segment = "{} {}".format(raw[i][0], ''.join(str(raw[i][1]))[1:-1]) - elif raw[i][0] == 'Z': - segment = "{},{}".format(raw[0][1][0], raw[0][1][1]) - else: - inkex.utils.debug("Segment error. Unknown command!") + for breakApartPath in breakApartPaths: + for element in breakApartPath: + elemGroup = self.svg.get_current_layer().add(inkex.Group(id="unwinding-" + element.get('id'))) - d = str(Path("{} {}".format(startPoint, segment))) - + #beginning point of the unwind band: + bbox = element.bounding_box() #shift the element to the bottom of the element + xmin = bbox.left + ymax = bbox.bottom + bbox.height * 0.1 #10% additional spacing + + csp = element.path.to_superpath() + subCount = len(element.path) + + #generate random colors; used to identify glue tab pairs + if self.options.colorize is True: + randomColorSet = [] + while len(randomColorSet) < subCount - 1: + r = lambda: random.randint(0,255) + newColor = '#%02X%02X%02X' % (r(),r(),r()) + if newColor not in randomColorSet: + randomColorSet.append(newColor) + + for sub in csp: + #generate new horizontal line data by measuring each segment + new = [] + new.append([sub[0]]) + i = 1 + topPathData = "m {:0.6f},{:0.6f} ".format(xmin, ymax) + bottomPathData = "m {:0.6f},{:0.6f} ".format(xmin, ymax + shifting) + lengths = [] + + if self.options.break_apart is True: + topLineGroup = self.svg.get_current_layer().add(inkex.Group(id="hline-top-" + element.get('id'))) + bottomLineGroup = self.svg.get_current_layer().add(inkex.Group(id="hline-bottom-" + element.get('id'))) + newOriginalPathGroup = self.svg.get_current_layer().add(inkex.Group(id="new-original-" + element.get('id'))) + elemGroup.append(topLineGroup) + elemGroup.append(bottomLineGroup) + self.svg.get_current_layer().append(newOriginalPathGroup) #we want this to be one level above unwound stuff + + if self.options.extrude is True: + vlinesGroup = self.svg.get_current_layer().add(inkex.Group(id="vlines-" + element.get('id'))) + elemGroup.append(vlinesGroup) + + while i <= len(sub) - 1: stroke_color = '#000000' - if self.options.colorize is True: + if self.options.colorize is True and self.options.break_apart is True: stroke_color =randomColorSet[i-1] - new_original_line_style = {'stroke':stroke_color,'stroke-width':'1px','fill':'none'} - self.drawline(d, "segmented-" + element.get('id'), newOriginalPathGroup, new_original_line_style) - - if self.options.keep_original is False: - element.delete() + + horizontal_line_style = {'stroke':stroke_color,'stroke-width':'1px','fill':'none'} + + length = bezier.cspseglength(new[-1][-1], sub[i]) #sub path length + segment = "h {:0.6f} ".format(length) + topPathData += segment + bottomPathData += segment + new[-1].append(sub[i]) #important line! + if self.options.break_apart is True: + self.drawline("m {:0.6f},{:0.0f} ".format(xmin + sum([length for length in lengths]), ymax) + segment, + "segmented-top-{}-{}".format(element.get('id'), i), topLineGroup, horizontal_line_style) + if self.options.extrude is True: + self.drawline("m {:0.6f},{:0.0f} ".format(xmin + sum([length for length in lengths]), ymax + shifting) + segment, + "segmented-bottom-{}-{}".format(element.get('id'), i), bottomLineGroup, horizontal_line_style) + lengths.append(length) + i += 1 + + + if self.options.break_apart is False: + self.drawline(topPathData, "combined-top-{0}".format(element.get('id')), elemGroup, horizontal_line_style) + if self.options.extrude is True: + self.drawline(bottomPathData, "combined-bottom-{0}".format(element.get('id')), elemGroup, horizontal_line_style) + + #draw as much vertical lines as segments in bezier + start + end vertical line + vertical_end_lines_style = {'stroke':'#000000','stroke-width':'1px','fill':'none'} + if self.options.extrude is True: + #render start line + self.drawline("m {:0.6f},{:0.6f} v {:0.6f}".format(xmin, ymax, shifting),"vline-{}-start".format(element.get('id')), vlinesGroup, vertical_end_lines_style) + #render divider lines + if self.options.render_vertical_dividers is True: + vertical_mid_lines_style = {'stroke':'#000000','stroke-width':'1px','fill':'none'} + if self.options.render_with_dashes is True: + vertical_mid_lines_style = {'stroke':'#000000','stroke-width':'1px',"stroke-dasharray":"2 2", 'fill':'none'} + x = 0 + for n in range(0, i-2): + x += lengths[n] + self.drawline("m {:0.6f},{:0.6f} v {:0.6f}".format(xmin + x, ymax, shifting),"vline-{}-{}".format(element.get('id'), n + 1), vlinesGroup, vertical_mid_lines_style) + #render end line + self.drawline("m {:0.6f},{:0.6f} v {:0.6f}".format(xmin + sum([length for length in lengths]), ymax, shifting),"vline-{}-end".format(element.get('id')), vlinesGroup, vertical_end_lines_style) + + if self.options.break_apart is True: + # Split (already broken apart) paths into detached segments + raw = Path(element.get("d")).to_arrays() #returns Uppercase Command Letters; does not include H, V + for i in range(len(raw)): + if i > 0: + + if raw[i-1][0] in ("M", "L"): + startPoint = "M {},{}".format(raw[i-1][1][0], raw[i-1][1][1]) + elif raw[i-1][0] == 'C': + startPoint = "M {},{}".format(raw[i-1][1][-2], raw[i-1][1][-1]) + else: + inkex.utils.debug("Start point error. Unknown command!") + + if raw[i][0] in ("M", "L"): + segment = " {},{}".format(raw[i][1][0], raw[i][1][1]) + elif raw[i][0] == 'C': + segment = "{} {}".format(raw[i][0], ''.join(str(raw[i][1]))[1:-1]) + elif raw[i][0] == 'Z': + segment = "{},{}".format(raw[0][1][0], raw[0][1][1]) + else: + inkex.utils.debug("Segment error. Unknown command!") + + d = str(Path("{} {}".format(startPoint, segment))) + + stroke_color = '#000000' + if self.options.colorize is True: + stroke_color =randomColorSet[i-1] + new_original_line_style = {'stroke':stroke_color,'stroke-width':'1px','fill':'none'} + self.drawline(d, "segmented-" + element.get('id'), newOriginalPathGroup, new_original_line_style) + + if self.options.keep_original is False: + element.delete() else: self.msg('Please select some paths first.')