diff --git a/extensions/fablabchemnitz/unwind_paths/unwind_paths.inx b/extensions/fablabchemnitz/unwind_paths/unwind_paths.inx index 637ecbad..bc760f90 100644 --- a/extensions/fablabchemnitz/unwind_paths/unwind_paths.inx +++ b/extensions/fablabchemnitz/unwind_paths/unwind_paths.inx @@ -2,15 +2,51 @@ Unwind Paths fablabchemnitz.de.unwind_paths - false - 10.000 - - - - - - - + + + + false + false + true + true + false + 10.000 + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + ../000_about_fablabchemnitz.svg + all diff --git a/extensions/fablabchemnitz/unwind_paths/unwind_paths.py b/extensions/fablabchemnitz/unwind_paths/unwind_paths.py index 8c0d0608..4416b78d 100644 --- a/extensions/fablabchemnitz/unwind_paths/unwind_paths.py +++ b/extensions/fablabchemnitz/unwind_paths/unwind_paths.py @@ -1,77 +1,213 @@ #!/usr/bin/env python3 """ +Extension for InkScape 1.0+ + +Paperfold is another flattener for triangle mesh files, heavily based on paperfoldmodels by Felix Scholz aka felixfeliz. + +Author: Mario Voigt / FabLab Chemnitz +Mail: mario.voigt@stadtfabrikanten.org +Date: 17.05.2021 +Last patch: 18.05.2021 +License: GNU GPL v3 + For each selected path element, this extension creates an additional path element consisting of horizontal line segments which are the same size as the original -line segments. Has option to extrude as a band (add height; adds vertical lines and another horizontal path) - -ToDo: -- option to colorize each line segment of the original curve (we need to split and group the original one to do this). We map the colors to the unwinded paths -- handle combined paths correctly -- copy style of input paths and only apply new colors +path segments. Has options to extrude as a band (adds height; adds vertical lines and another horizontal path as bottom enclosure) +ToDos: +- option to render separate rectangle shapes +- option to duplicate vertical lines and then to group each 4 lines into one rect-shape like group +- option to colorize vertical line start + end +- option to add glue tabs/flaps +- option to add length text to each segment +- option to add segment/surface numbers """ - +import copy import inkex -from inkex import bezier +from inkex import bezier, Path, CubicSuperPath from lxml import etree import math +import random class UnwindPaths(inkex.EffectExtension): #draw an SVG line segment between the given (raw) points - def drawline(self, pathData, name, parent): - line_style = {'stroke':'#000000','stroke-width':'1px','fill':'none'} - line_attribs = {'style' : str(inkex.Style(line_style)), - inkex.addNS('label','inkscape') : name, - 'd' : pathData} + def drawline(self, pathData, name, parent, line_style): + line_attribs = {'style' : str(inkex.Style(line_style)), inkex.addNS('label','inkscape') : name, 'd' : pathData} line = etree.SubElement(parent, inkex.addNS('path','svg'), line_attribs ) - 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('--render_vertical_dividers', type=inkex.Boolean, default=False) + pars.add_argument('--render_with_dashes', type=inkex.Boolean, default=False) pars.add_argument('--extrude', type=inkex.Boolean, default=False) pars.add_argument('--extrude_height', type=float, default=10.000) pars.add_argument('--unit', default="mm") + pars.add_argument('--colorize', type=inkex.Boolean, default=False, help="Requires enabled 'Break apart' option") + + #if multiple curves are inside the path we split (break apart) + 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:]) + if len(subPaths) > 1: + 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) + else: + breakelements.append(element) + for child in element.getchildren(): + self.breakContours(child, breakelements) + return breakelements def effect(self): - path_num = 0 shifting = self.svg.unittouu(str(self.options.extrude_height) + self.options.unit) - - for element in self.svg.selection.filter(inkex.PathElement).values(): - 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 - - for sub in element.path.to_superpath(): - 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 = [] - while i <= len(sub) - 1: - length = bezier.cspseglength(new[-1][-1], sub[i]) #sub path length - lengths.append(length) - segment = "l {:0.6f},{:0.0f} ".format(length, 0) - topPathData += segment - bottomPathData += segment - new[-1].append(sub[i]) - i += 1 - - self.drawline(topPathData, "hline-top-{0}".format(element.get('id')), elemGroup) - if self.options.extrude is True: - self.drawline(bottomPathData, "hline-bottom-{0}".format(element.get('id')), elemGroup) + if len(self.svg.selected) > 0: + #we break apart combined paths to get distinct contours + for element in self.svg.selection.filter(inkex.PathElement).values(): + breakApartPaths = 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) + elemGroup.append(newOriginalPathGroup) + + 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 - self.drawline("m {:0.6f},{:0.6f} v {:0.6f}".format(xmin, ymax, shifting),"vline{0}".format(path_num), elemGroup) - self.drawline("m {:0.6f},{:0.6f} v {:0.6f}".format(xmin + sum([length for length in lengths]), ymax, shifting),"vline{0}".format(path_num), elemGroup) - x = 0 - for n in range(0, i-1): - x += lengths[n] - self.drawline("m {:0.6f},{:0.6f} v {:0.6f}".format(xmin + x, ymax, shifting),"vline{0}".format(path_num), elemGroup) + 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.') + return if __name__ == '__main__': UnwindPaths().run()