From c6546af0434bc4fca741c03462e42ff272395a65 Mon Sep 17 00:00:00 2001 From: Mario Voigt Date: Fri, 31 Jul 2020 16:05:02 +0200 Subject: [PATCH] Added Origami Patterns --- OrigamiPatterns/Hypar.py | 112 +++++ OrigamiPatterns/Kresling.py | 154 ++++++ OrigamiPatterns/Kresling_full.py | 89 ++++ OrigamiPatterns/Path.py | 466 ++++++++++++++++++ OrigamiPatterns/Pattern.py | 263 ++++++++++ OrigamiPatterns/Pleat_Circular.py | 91 ++++ OrigamiPatterns/Template.py | 97 ++++ OrigamiPatterns/Waterbomb.py | 103 ++++ OrigamiPatterns/__init__.py | 0 fablabchemnitz_origami_patterns_kresling.inx | 92 ++++ ...emnitz_origami_patterns_pleat_circular.inx | 67 +++ ...bchemnitz_origami_patterns_pleat_hypar.inx | 78 +++ fablabchemnitz_origami_patterns_template.inx | 90 ++++ fablabchemnitz_origami_patterns_waterbomb.inx | 73 +++ 14 files changed, 1775 insertions(+) create mode 100644 OrigamiPatterns/Hypar.py create mode 100644 OrigamiPatterns/Kresling.py create mode 100644 OrigamiPatterns/Kresling_full.py create mode 100644 OrigamiPatterns/Path.py create mode 100644 OrigamiPatterns/Pattern.py create mode 100644 OrigamiPatterns/Pleat_Circular.py create mode 100644 OrigamiPatterns/Template.py create mode 100644 OrigamiPatterns/Waterbomb.py create mode 100644 OrigamiPatterns/__init__.py create mode 100644 fablabchemnitz_origami_patterns_kresling.inx create mode 100644 fablabchemnitz_origami_patterns_pleat_circular.inx create mode 100644 fablabchemnitz_origami_patterns_pleat_hypar.inx create mode 100644 fablabchemnitz_origami_patterns_template.inx create mode 100644 fablabchemnitz_origami_patterns_waterbomb.inx diff --git a/OrigamiPatterns/Hypar.py b/OrigamiPatterns/Hypar.py new file mode 100644 index 00000000..699b563d --- /dev/null +++ b/OrigamiPatterns/Hypar.py @@ -0,0 +1,112 @@ +#! /usr/bin/env python3 + +import numpy as np +from math import pi, tan, sqrt, sin, cos +import inkex +from Path import Path +from Pattern import Pattern + +class Hypar(Pattern): + def __init__(self): + Pattern.__init__(self) # Must be called in order to parse common options + self.add_argument("-p", "--pattern", default="template1", help="Origami pattern") + self.add_argument("--radius", type=float, default=10.0, help="Radius of tower (mm)") + self.add_argument("--sides", type=int, default=4, help="Number of polygon sides") + self.add_argument("--rings", type=int, default=7, help="Number of rings") + self.add_argument("--simplify_center", type=inkex.Boolean, default=0, help="Simplify center") + + def generate_path_tree(self): + """ Specialized path generation for your origami pattern + """ + # retrieve saved parameters + unit_factor = self.calc_unit_factor() + vertex_radius = self.options.vertex_radius * unit_factor + pattern = self.options.pattern + radius = self.options.radius * unit_factor + sides = self.options.sides + rings = self.options.rings + simplify_center = self.options.simplify_center + sin_ = sin(pi / float(sides)) + a = radius*sin_ # half of length of polygon side + H = radius*sqrt(1 - sin_**2) + + polygon = Path.generate_polygon(sides, radius, 'e') + + # # OLD diagonals generation with universal creases + # diagonals = [] + # for i in range(sides): + # diagonals.append(Path([(0, 0), polygon.points[i]], 'u')) + # points = [(x, y) for x, y in polygon.points] + # diagonals = diagonals + [Path.generate_separated_paths(points, 'm')] + + # # modify center if needed + # if simplify_center: + # for i in range(sides): + # if i % 2 == 0: + # p2 = diagonals[i].points[1] + # diagonals[i].points[0] = (1. / (rings + 1) * p2[0], 1. / (rings + 1) * p2[1]) + + # separate generic closed ring to create edges + self.edge_points = polygon.points + + # vertex and diagonal lines creation + vertex_line = [] + diagonal_line = [] + for i in range(1, rings + 2): + y1 = a * (float(i - 1) / (rings + 1.)) + x1 = H * float(i - 1) / (rings + 1.) + y2 = a * (float(i) / (rings + 1.)) + x2 = H * float(i) / (rings + 1.) + vertex_line.append((Path((x2, y2), style='p', radius=vertex_radius))) + diagonal_line.append((Path([(x1, y1), (x2, y2)], style='m' if i % 2 else 'v'))) + + # rotation of vertices and diagonals for completing the drawing + diagonals = [] + vertices = [Path((0, 0), style='p', radius=vertex_radius)] + for i in range(sides): + vertices = vertices+Path.list_rotate(vertex_line, i * 2 * pi / float(sides)) + diagonals = diagonals+[Path.list_rotate(diagonal_line, i * 2 * pi / float(sides))] + + # modify center if needed + if simplify_center: + for i in range(sides): + if i % 2 == 0: + del diagonals[i][0] + + # inkex.debug(len(diagonals)) + # inkex.debug(len(diagonals[0])) + # diagonals = diagonals + diagonal + + # scale generic closed ring to create inner rings + inner_rings = [] + for i in range(rings + 1): + inner_rings.append(polygon * (float(i)/(rings+1))) + inner_rings[i].style = 'v' if i % 2 else 'm' + + # create points for zig zag pattern + zig_zags = [] + if pattern != "classic": + zig_zag = [] + for i in range(1, rings + 1): + y_out = a * ((i + 1.) / (rings + 1.)) + y_in = a * (float(i) / (rings + 1.)) + x_out = H * (i + 1.) / (rings + 1.) + x_in = H * float(i) / (rings + 1.) + + if pattern == "alternate_asymmetric" and i % 2: + zig_zag.append(Path([(x_in, -y_in), (x_out, +y_out)], style='u')) + else: + zig_zag.append(Path([(x_in, +y_in), (x_out, -y_out)], style='u')) + + # reflect zig zag pattern to create all sides + zig_zags.append(zig_zag) + for i in range(sides - 1): + points = diagonals[i][0].points + zig_zags.append(Path.list_reflect(zig_zags[i], points[0], points[1])) + + self.translate = (radius, radius) + self.path_tree = [diagonals, zig_zags, inner_rings, vertices] + +# Main function, creates an instance of the Class and calls inkex.affect() to draw the origami on inkscape +if __name__ == '__main__': + Hypar().run() \ No newline at end of file diff --git a/OrigamiPatterns/Kresling.py b/OrigamiPatterns/Kresling.py new file mode 100644 index 00000000..6c3ef0d1 --- /dev/null +++ b/OrigamiPatterns/Kresling.py @@ -0,0 +1,154 @@ +#! /usr/bin/env python3 + +from math import pi, sin, cos, tan, acos, sqrt +import inkex +from Path import Path +from Pattern import Pattern + +class Kresling(Pattern): + def __init__(self): + Pattern.__init__(self) # Must be called in order to parse common options + self.add_argument("-p", "--pattern", default="kresling", help="Origami pattern") + self.add_argument("--lines", type=int, default=1, help="Number of lines") + self.add_argument("--sides", type=int, default=3, help="Number of polygon sides") + self.add_argument("--add_attachment", type=inkex.Boolean, default=False, help="Add attachment?") + self.add_argument("--attachment_percentage", type=float, default=100., help="Length percentage of extra facet") + self.add_argument("--mirror_cells", type=inkex.Boolean, default=False, help="Mirror odd cells?") + + @staticmethod + def generate_kresling_zigzag(sides, radius, angle_ratio, add_attachment): + + theta = (pi / 2.) * (1 - 2. / sides) + l = 2. * radius * cos(theta * (1. - angle_ratio)) + a = 2. * radius * sin(pi / sides) + # b = sqrt(a * a + l * l - 2 * a * l * cos(angle_ratio * theta)) + # phi = abs(acos((l * l + b * b - a * a) / (2 * l * b))) + # gamma = pi / 2 - angle_ratio * theta - phi + dy = l * sin(theta * angle_ratio) + dx = l * cos(theta * angle_ratio) - a + + points = [] + styles = [] + + for i in range(sides): + points.append((i * a, 0)) + points.append(((i + 1) * a + dx, -dy)) + styles.append('v') + if i != sides - 1: + styles.append('m') + elif add_attachment: + points.append((sides * a, 0)) + styles.append('m') + + path = Path.generate_separated_paths(points, styles) + return path + + def generate_path_tree(self): + """ Specialized path generation for Waterbomb tesselation pattern + """ + unit_factor = self.calc_unit_factor() + vertex_radius = self.options.vertex_radius * unit_factor + lines = self.options.lines + sides = self.options.sides + radius = self.options.radius * unit_factor + angle_ratio = self.options.angle_ratio + mirror_cells = self.options.mirror_cells + + theta = (pi/2.)*(1 - 2./sides) + l = 2.*radius*cos(theta*(1.-angle_ratio)) + a = 2.*radius*sin(pi/sides) + # b = sqrt(a*a + l*l - 2*a*l*cos(angle_ratio*theta)) + # phi = abs(acos((l*l + b*b - a*a)/(2*l*b))) + # gamma = pi/2 - angle_ratio*theta - phi + # dy = b*cos(gamma) + # dx = b*sin(gamma) + dy = l * sin(theta * angle_ratio) + dx = l * cos(theta * angle_ratio) - a + + add_attachment = self.options.add_attachment + attachment_percentage = self.options.attachment_percentage/100. + attachment_height = a*(attachment_percentage-1)*tan(angle_ratio*theta) + + vertices = [] + for i in range(sides + 1): + for j in range(lines + 1): + if mirror_cells: + vertices.append(Path((dx*((lines - j)%2) + a*i, dy*j), style='p', radius=vertex_radius)) + else: + vertices.append(Path((dx*(lines - j) + a*i, dy*j), style='p', radius=vertex_radius)) + + # create a horizontal grid, then offset each line according to angle + grid_h = Path.generate_hgrid([0, a * sides], [0, dy * lines], lines, 'm') + + + if not mirror_cells: + # shift every mountain line of the grid to the right by increasing amounts + grid_h = Path.list_add(grid_h, [(i * dx, 0) for i in range(lines - 1, 0, -1)]) + else: + # shift every OTHER mountain line of the grid a bit to the right + grid_h = Path.list_add(grid_h, [((i%2)*dx, 0) for i in range(lines-1, 0, -1)]) + if add_attachment: + for i in range(lines%2, lines-1, 2): + # hacky solution, changes length of every other mountain line + grid_h[i].points[1-i%2] = (grid_h[i].points[1-i%2][0] + a*attachment_percentage, grid_h[i].points[1-i%2][1]) + + + # create MV zigzag for Kresling pattern + zigzag = Kresling.generate_kresling_zigzag(sides, radius, angle_ratio, add_attachment) + zigzags = [] + + # duplicate zigzag pattern for desired number of cells + if not mirror_cells: + for i in range(lines): + zigzags.append(Path.list_add(zigzag, (i * dx, (lines - i) * dy))) + else: + zigzag_mirror = Path.list_reflect(zigzag, (0, lines * dy / 2), (dx, lines * dy / 2)) + for i in range(lines): + if i % 2 == 1: + zigzags.append(Path.list_add(zigzag_mirror, (0, -(lines - i + (lines-1)%2) * dy))) + else: + zigzags.append(Path.list_add(zigzag, (0, (lines - i) * dy))) + + # create edge strokes + if not mirror_cells: + self.edge_points = [ + (a * sides , dy * lines), # bottom right + (0 , dy * lines), # bottom left + (dx * lines , 0), # top left + (dx * lines + a * sides, 0)] # top right + + if add_attachment: + for i in range(lines): + x = dx * (lines - i) + a * (sides + attachment_percentage) + self.edge_points.append((x, dy * i)) + self.edge_points.append((x, dy * i - attachment_height)) + if i != lines - 1: + self.edge_points.append((x-dx-a*attachment_percentage, dy * (i + 1))) + pass + + else: + self.edge_points = [(a * sides + (lines % 2)*dx, 0)] + + for i in range(lines+1): + self.edge_points.append([((lines+i) % 2)*dx, dy*i]) + + self.edge_points.append([a * sides + ((lines+i) %2)*dx, lines*dy]) + + if add_attachment: + for i in range(lines + 1): + + if not i%2 == 0: + self.edge_points.append([a*sides + (i%2)*(dx+a*attachment_percentage), dy*(lines - i) - (i%2)*attachment_height]) + self.edge_points.append([a*sides + (i%2)*(dx+a*attachment_percentage), dy*(lines - i)]) + if (i != lines): + self.edge_points.append([a * sides + (i % 2) * (dx + a * attachment_percentage), dy * (lines - i) + (i % 2) * attachment_height]) + else: + self.edge_points.append([a * sides + (i % 2) * (dx + a * attachment_percentage), dy * (lines - i)]) + else: + for i in range(lines + 1): + self.edge_points.append([a*sides + (i%2)*dx, dy*(lines - i)]) + + self.path_tree = [grid_h, zigzags, vertices] + +if __name__ == '__main__': + Kresling().run() \ No newline at end of file diff --git a/OrigamiPatterns/Kresling_full.py b/OrigamiPatterns/Kresling_full.py new file mode 100644 index 00000000..5d55d004 --- /dev/null +++ b/OrigamiPatterns/Kresling_full.py @@ -0,0 +1,89 @@ +#! /usr/bin/env python3 + +from math import sin, cos, sqrt, asin, pi, ceil +import inkex +from Path import Path +from Pattern import Pattern +from Kresling import Kresling + + +class Kresling_Full(Kresling): + + def __init__(self): + """ Constructor + """ + Kresling.__init__(self) # Must be called in order to parse common options + + self.add_argument('--measure_value', type=float, default=10.0, help="Length") + self.add_argument('--measure_type', default=60, help="Type of length") + self.add_argument('--parameter_type', default=60, help="Type of parameter") + self.add_argument('--radial_ratio', type=float, default=0.5, help="Radial ratio") + self.add_argument('--angle_ratio', type=float, default=0.5, help="Anle ratio") + self.add_argument('--lambdatheta', type=float, default=45, help="lambdatheta") + + def generate_path_tree(self): + """ Convert radial to angular ratio, then call regular Kresling constructor + """ + n = self.options.sides + theta = pi*(n-2)/(2*n) + # define ratio parameter + parameter = self.options.parameter_type + if parameter == 'radial_ratio': + radial_ratio = self.options.radial_ratio + max_radial_ratio = sin((pi/4)*(1. - 2./n)) + if radial_ratio > max_radial_ratio: + inkex.errormsg(_("For polygon of {} sides, the maximal radial ratio is = {}".format(n, max_radial_ratio))) + radial_ratio = max_radial_ratio + self.options.angle_ratio = 1 - 2*n*asin(radial_ratio)/((n-2)*pi) + + elif parameter == 'lambdatheta': + lambdatheta = self.options.lambdatheta + angle_min = 45. * (1 - 2. / n) + angle_max = 2 * angle_min + if lambdatheta < angle_min: + inkex.errormsg(_( + "For polygon of {} sides, phi must be between {} and {} degrees, \nsetting lambda*theta = {}\n".format( + n, angle_min, angle_max, angle_min))) + lambdatheta = angle_min + elif lambdatheta > angle_max: + inkex.errormsg(_( + "For polygon of {} sides, phi must be between {} and {} degrees, \nsetting lambda*theta = {}\n".format( + n, angle_min, angle_max, angle_max))) + lambdatheta = angle_max + self.options.angle_ratio = lambdatheta * n / (90. * (n - 2.)) + + + # define some length + mtype = self.options.measure_type + mvalue = self.options.measure_value + angle_ratio = self.options.angle_ratio + if mtype == 'a': + radius = 0.5*mvalue / (sin(pi/n)) + if mtype == 'b': + A = cos(theta*(1-angle_ratio)) + B = sin(pi/n) + C = cos(theta*angle_ratio) + radius = 0.5*mvalue / sqrt(A**2 + B**2 - 2*A*B*C) + elif mtype == 'l': + radius = 0.5*mvalue/cos(theta*(1-angle_ratio)) + elif mtype == 'radius_external': + radius = mvalue + elif mtype == 'radius_internal': + radius = mvalue/(sin(theta*(1-angle_ratio))) + elif mtype == 'diameter_external': + radius = 0.5*mvalue + elif mtype == 'diameter_internal': + radius = 0.5*mvalue/sin(theta*(1-angle_ratio)) + + # inkex.errormsg(_("Value = {}, Mode = {}, Radius = {}".format(mvalue, mtype, radius))) + + if self.options.pattern == 'mirrowed': + self.options.mirror_cells = True + else: + self.options.mirror_cells = False + self.options.radius = radius + + Kresling.generate_path_tree(self) + +if __name__ == '__main__': + Kresling_Full().run() \ No newline at end of file diff --git a/OrigamiPatterns/Path.py b/OrigamiPatterns/Path.py new file mode 100644 index 00000000..ceab873c --- /dev/null +++ b/OrigamiPatterns/Path.py @@ -0,0 +1,466 @@ +#! /usr/bin/env python3 + +import inkex +from lxml import etree +from math import sin, cos, pi + +class Path: + """ Class that defines an svg stroke to be drawn in Inkscape + + Attributes + --------- + points: tuple or list of tuples + Points defining stroke lines. + style: str + Single character defining style of stroke. Default values are: + 'm' for mountain creases + 'v' for valley creases + 'e' for edge borders + Extra possible values: + 'u' for universal creases + 's' for semicreases + 'c' for kirigami cuts + closed: bool + Tells if desired path should contain a last stroke from the last point to the first point, closing the path + radius: float + If only one point is given, it's assumed to be a circle and radius sets the radius + + + Methods + --------- + invert(self) + Inverts path + + Overloaded Operators + --------- + __add__(self, offsets) + Adding a tuple to a Path returns a new path with all points having an offset defined by the tuple + + __mul__(self, transform) + Define multiplication of a Path to a vector in complex exponential representation + + + Static Methods + --------- + draw_paths_recursively(path_tree, group, styles_dict) + Draws strokes defined on "path_tree" to "group". Styles dict maps style of path_tree element to the definition + of the style. Ex.: + if path_tree[i].style = 'm', styles_dict must have an element 'm'. + + generate_hgrid(cls, xlims, ylims, nb_of_divisions, style, include_edge=False) + Generate list of Path instances, in which each Path is a stroke defining a horizontal grid dividing the space + xlims * ylims nb_of_divisions times. + + generate_vgrid(cls, xlims, ylims, nb_of_divisions, style, include_edge=False) + Generate list of Path instances, in which each Path is a stroke defining a vertical grid dividing the space + xlims * ylims nb_of_divisions times. + + generate_separated_paths(cls, points, styles, closed=False) + Generate list of Path instances, in which each Path is the stroke between each two point tuples, in case each + stroke must be handled separately + + reflect(cls, path, p1, p2) + Reflects each point of path on line defined by two points and return new Path instance with new reflected points + + list_reflect(cls, paths, p1, p2) + Generate list of new Path instances, rotation each path by transform + + list_rotate(cls, paths, theta, translation=(0, 0)) + Generate list of new Path instances, rotation each path by transform + + list_add(cls, paths, offsets) + Generate list of new Path instances, adding a different tuple for each list + + + """ + + def __init__(self, points, style, closed=False, invert=False, radius=0.1, separated=False): + """ Constructor + + Parameters + --------- + points: list of 2D tuples + stroke will connect all points + style: str + Single character defining style of stroke. For use with the OrigamiPatterns class (probably the only + project that will ever use this file) the default values are: + 'm' for mountain creases + 'v' for valley creases + 'e' for edge borders + closed: bool + if true, last point will be connected to first point at the end + invert: bool + if true, stroke will start at the last point and go all the way to the first one + """ + if type(points) == list and len(points) != 1: + self.type = 'linear' + if invert: + self.points = points[::-1] + else: + self.points = points + + elif (type(points) == list and len(points) == 1): + self.type = 'circular' + self.points = points + self.radius = radius + + elif (type(points) == tuple and len(points) == 2): + self.type = 'circular' + self.points = [points] + self.radius = radius + + else: + raise TypeError("Points must be tuple of length 2 (for a circle) or a list of tuples of length 2 each") + + self.style = style + self.closed = closed + + def invert(self): + """ Inverts path + """ + self.points = self.points[::-1] + + """ + Draw path recursively + - Static method + - Draws strokes defined on "path_tree" to "group" + - Inputs: + -- path_tree [nested list] of Path instances + -- group [etree.SubElement] + -- styles_dict [dict] containing all styles for path_tree + """ + @staticmethod + def draw_paths_recursively(path_tree, group, styles_dict): + """ Static method, draw list of Path instances recursively + """ + for subpath in path_tree: + if type(subpath) == list: + if len(subpath) == 1: + subgroup = group + else: + subgroup = etree.SubElement(group, 'g') + Path.draw_paths_recursively(subpath, subgroup, styles_dict) + else: + if styles_dict[subpath.style]['draw']: + if subpath.type == 'linear': + + points = subpath.points + path = 'M{},{} '.format(*points[0]) + for i in range(1, len(points)): + path = path + 'L{},{} '.format(*points[i]) + if subpath.closed: + path = path + 'L{},{} Z'.format(*points[0]) + + attribs = {'style': str(inkex.Style(styles_dict[subpath.style])), 'd': path} + etree.SubElement(group, inkex.addNS('path', 'svg'), attribs) + else: + attribs = {'style': str(inkex.Style(styles_dict[subpath.style])), + 'cx': str(subpath.points[0][0]), 'cy': str(subpath.points[0][1]), + 'r': str(subpath.radius)} + etree.SubElement(group, inkex.addNS('circle', 'svg'), attribs) + + @classmethod + def generate_hgrid(cls, xlims, ylims, nb_of_divisions, style, include_edge=False): + """ Generate list of Path instances, in which each Path is a stroke defining + a horizontal grid dividing the space xlims * ylims nb_of_divisions times. + + All lines are alternated, to minimize Laser Cutter unnecessary movements + + Parameters + --------- + xlims: tuple + Defines x_min and x_max for space that must be divided. + ylims: tuple + Defines y_min and y_max for space that must be divided. + nb_of_divisions: int + Defines how many times it should be divided. + style: str + Single character defining style of stroke. + include_edge: bool + Defines if edge should be drawn or not. + + Returns + --------- + paths: list of Path instances + """ + rect_len = (ylims[1] - ylims[0])/nb_of_divisions + hgrid = [] + for i in range(1 - include_edge, nb_of_divisions + include_edge): + hgrid.append(cls([(xlims[0], ylims[0]+i*rect_len), + (xlims[1], ylims[0]+i*rect_len)], + style=style, invert=i % 2 == 0)) + return hgrid + + @classmethod + def generate_vgrid(cls, xlims, ylims, nb_of_divisions, style, include_edge=False): + """ Generate list of Path instances, in which each Path is a stroke defining + a vertical grid dividing the space xlims * ylims nb_of_divisions times. + + All lines are alternated, to minimize Laser Cutter unnecessary movements + + Parameters + --------- + -> refer to generate_hgrid + + Returns + --------- + paths: list of Path instances + """ + rect_len = (xlims[1] - xlims[0])/nb_of_divisions + vgrid = [] + for i in range(1 - include_edge, nb_of_divisions + include_edge): + vgrid.append(cls([(xlims[0]+i*rect_len, ylims[0]), + (xlims[0]+i*rect_len, ylims[1])], + style=style, invert=i % 2 == 0)) + return vgrid + + @classmethod + def generate_polygon(cls, sides, radius, style, center=(0, 0)): + points = [] + for i in range(sides): + points.append((radius * cos((1 + i * 2) * pi / sides), + radius * sin((1 + i * 2) * pi / sides))) + return Path(points, style, closed=True) + + @classmethod + def generate_separated_paths(cls, points, styles, closed=False): + """ Generate list of Path instances, in which each Path is the stroke + between each two point tuples, in case each stroke must be handled separately. + + Returns + --------- + paths: list + list of Path instances + """ + paths = [] + if type(styles) == str: + styles = [styles] * (len(points) - 1 + int(closed)) + elif len(styles) != len(points) - 1 + int(closed): + raise TypeError("Number of paths and styles don't match") + for i in range(len(points) - 1 + int(closed)): + j = (i+1)%len(points) + paths.append(cls([points[i], points[j]], + styles[i])) + return paths + + def __add__(self, offsets): + """ " + " operator overload. + Adding a tuple to a Path returns a new path with all points having an offset + defined by the tuple + """ + if type(offsets) == list: + if len(offsets) != 1 or len(offsets) != len(self.points): + raise TypeError("Paths can only be added by a tuple of a list of N tuples, " + "where N is the same number of points") + + elif type(offsets) != tuple: + raise TypeError("Paths can only be added by tuples") + else: + offsets = [offsets] * len(self.points) + + # if type(self.points) == list: + points_new = [] + for point, offset in zip(self.points, offsets): + points_new.append((point[0]+offset[0], + point[1]+offset[1])) + + if self.type == 'circular': + radius = self.radius + else: + radius = 0.2 + + # if self.type == 'circular' else 0.1 + + return Path(points_new, self.style, self.closed, radius=radius) + + @classmethod + def list_add(cls, paths, offsets): + """ Generate list of new Path instances, adding a different tuple for each list + + Parameters + --------- + paths: Path or list + list of N Path instances + offsets: tuple or list + list of N tuples + + Returns + --------- + paths_new: list + list of N Path instances + """ + if type(paths) == Path and type(offsets) == tuple: + paths = [paths] + offsets = [offsets] + elif type(paths) == list and type(offsets) == tuple: + offsets = [offsets] * len(paths) + elif type(paths) == Path and type(offsets) == list: + paths = [paths] * len(offsets) + elif type(paths) == list and type(offsets) == list: + if len(paths) == 1: + paths = [paths[0]] * len(offsets) + elif len(offsets) == 1: + offsets = [offsets[0]] * len(paths) + elif len(offsets) != len(paths): + raise TypeError("List of paths and list of tuples must have same length. {} paths and {} offsets " + " where given".format(len(paths), len(offsets))) + else: + pass + + paths_new = [] + for path, offset in zip(paths, offsets): + paths_new.append(path+offset) + + return paths_new + + def __mul__(self, transform): + """ " * " operator overload. + Define multiplication of a Path to a vector in complex exponential representation + + Parameters + --------- + transform: float of tuple of length 2 or 4 + if float, transform represents magnitude + Example: path * 3 + if tuple length 2, transform[0] represents magnitude and transform[1] represents angle of rotation + Example: path * (3, pi) + if tuple length 4, transform[2],transform[3] define a different axis of rotation + Example: path * (3, pi, 1, 1) + """ + points_new = [] + + # "temporary" (probably permanent) compatibility hack + try: + long_ = long + except: + long_ = int + + if isinstance(transform, (int, long_, float)): + for p in self.points: + points_new.append((transform * p[0], + transform * p[1])) + + elif isinstance(transform, (list, tuple)): + if len(transform) == 2: + u = transform[0]*cos(transform[1]) + v = transform[0]*sin(transform[1]) + x_, y_ = 0, 0 + elif len(transform) == 4: + u = transform[0]*cos(transform[1]) + v = transform[0]*sin(transform[1]) + x_, y_ = transform[2:] + else: + raise IndexError('Paths can only be multiplied by a number or a tuple/list of length 2 or 4') + + for p in self.points: + x, y = p[0]-x_, p[1]-y_ + points_new.append((x_ + x * u - y * v, + y_ + x * v + y * u)) + else: + raise TypeError('Paths can only be multiplied by a number or a tuple/list of length 2 or 4') + + if self.type == 'circular': + radius = self.radius + else: + radius = 0.2 + + return Path(points_new, self.style, self.closed, radius=radius) + + @classmethod + def list_rotate(cls, paths, theta, translation=(0, 0)): + """ Generate list of new Path instances, rotation each path by transform + + Parameters + --------- + paths: Path or list + list of N Path instances + theta: float (radians) + angle of rotation + translation: tuple or list 2 + axis of rotation + + Returns + --------- + paths_new: list + list of N Path instances + """ + if len(translation) != 2: + TypeError("Translation must have length 2") + + if type(paths) != list: + paths = [paths] + + paths_new = [] + for path in paths: + paths_new.append(path*(1, theta, translation[0], translation[1])) + + if len(paths_new) == 1: + paths_new = paths_new[0] + return paths_new + + # TODO: + # Apparently it's not working properly, must be debugged and tested + @classmethod + def reflect(cls, path, p1, p2): + """ Reflects each point of path on line defined by two points and return new Path instance with new reflected points + + Parameters + --------- + path: Path + p1: tuple or list of size 2 + p2: tuple or list of size 2 + + Returns + --------- + path_reflected: Path + """ + + (x1, y1) = p1 + (x2, y2) = p2 + + if x1 == x2 and y1 == y2: + ValueError("Duplicate points don't define a line") + elif x1 == x2: + t_x = [-1, 0, 2*x1, 1] + t_y = [0, 1, 0, 1] + else: + m = (y2 - y1)/(x2 - x1) + t = y1 - m*x1 + t_x = [1 - m**2, 2*m, -2*m*t, m**2 + 1] + t_y = [2*m, m**2 - 1, +2*t, m**2 + 1] + + points_new = [] + for p in path.points: + x_ = (t_x[0]*p[0] + t_x[1]*p[1] + t_x[2]) / t_x[3] + y_ = (t_y[0]*p[0] + t_y[1]*p[1] + t_y[2]) / t_y[3] + points_new.append((x_, y_)) + + return Path(points_new, path.style, path.closed) + + # TODO: + # Apparently it's not working properly, must be debugged and tested + @classmethod + def list_reflect(cls, paths, p1, p2): + """ Generate list of new Path instances, rotation each path by transform + + Parameters + --------- + paths: Path or list + list of N Path instances + p1: tuple or list of size 2 + p2: tuple or list of size 2 + + Returns + --------- + paths_new: list + list of N Path instances + """ + + if type(paths) == Path: + paths = [paths] + + paths_new = [] + for path in paths: + paths_new.append(Path.reflect(path, p1, p2)) + + return paths_new \ No newline at end of file diff --git a/OrigamiPatterns/Pattern.py b/OrigamiPatterns/Pattern.py new file mode 100644 index 00000000..370b0178 --- /dev/null +++ b/OrigamiPatterns/Pattern.py @@ -0,0 +1,263 @@ +#! /usr/bin/env python3 + +import os +from abc import abstractmethod +from lxml import etree +from Path import Path, inkex + +class Pattern(inkex.Effect): + @abstractmethod + def generate_path_tree(self): + """ Generate nested list of Path instances + Abstract method, must be defined in all child classes + """ + pass + + def __init__(self): + inkex.Effect.__init__(self) # initialize the super class + self.add_argument = self.arg_parser.add_argument + self.add_argument("-u", "--units", default='mm', help="Units this dialog is using") + + # self.add_argument("-a", "--add_attachment", type=inkex.Boolean, default=False, help="command line help") + # self.add_argument("", "--accuracy", type=int, default=0, help="command line help") + + # -------------------------------------------------------------------------------------------------------------- + # mountain options + self.add_argument('-m', '--mountain_stroke_color', default=4278190335, help='The mountain creases color.') + self.add_argument('--mountain_stroke_width', type=float, default=0.1, help='Width of mountain strokes.') + self.add_argument('--mountain_dashes_len', type=float, default=1.0, help='Mountain dash + gap length.') + self.add_argument('--mountain_dashes_duty', type=float, default=0.5, help='Mountain dash duty cycle.') + self.add_argument('--mountain_dashes_bool', type=inkex.Boolean, default=True, help='Dashed strokes?') + self.add_argument('--mountain_bool', type=inkex.Boolean, default=True, help='Draw mountains?') + + # -------------------------------------------------------------------------------------------------------------- + # valley options + self.add_argument('-v', '--valley_stroke_color', default=65535, help='The valley creases color.') + self.add_argument('--valley_stroke_width', type=float, default=0.1, help='Width of valley strokes.') + self.add_argument('--valley_dashes_len', type=float, default=1.0, help='Valley dash + gap length.') + self.add_argument('--valley_dashes_duty', type=float, default=0.25, help='Valley dash duty cycle.') + self.add_argument('--valley_dashes_bool', type=inkex.Boolean, default=True, help='Dashed strokes?') + self.add_argument('--valley_bool', type=inkex.Boolean, default=True, help='Draw valleys?') + + # -------------------------------------------------------------------------------------------------------------- + # edge options + self.add_argument('-e', '--edge_stroke_color', default=255, help='The mountain creases color.') + self.add_argument('--edge_stroke_width', type=float, default=0.1, help='Width of edge strokes.') + self.add_argument('--edge_dashes_len', type=float, default=1.0, help='Edge dash + gap length.') + self.add_argument('--edge_dashes_duty', type=float, default=0.25, help='Edge dash duty cycle.') + self.add_argument('--edge_dashes_bool', type=inkex.Boolean, default=False, help='Dashed strokes?') + self.add_argument('--edge_bool', type=inkex.Boolean, default=True, help='Draw edges?') + self.add_argument('--edge_single_path', type=inkex.Boolean, default=True, help='Edges as single path?') + + # -------------------------------------------------------------------------------------------------------------- + # universal crease options + self.add_argument('--universal_stroke_color', default=4278255615, help='The universal creases color.') + self.add_argument('--universal_stroke_width', type=float, default=0.1, help='Width of universal strokes.') + self.add_argument('--universal_dashes_len', type=float, default=1.0, help='Universal dash + gap length.') + self.add_argument('--universal_dashes_duty', type=float, default=0.25, help='Universal dash duty cycle.') + self.add_argument('--universal_dashes_bool', type=inkex.Boolean, default=False, help='Dashed strokes?') + self.add_argument('--universal_bool', type=inkex.Boolean, default=True, help='Draw universal creases?') + + # -------------------------------------------------------------------------------------------------------------- + # semicrease options + self.add_argument('--semicrease_stroke_color', default=4294902015, help='The semicrease creases color.') + self.add_argument('--semicrease_stroke_width', type=float, default=0.1, help='Width of semicrease strokes.') + self.add_argument('--semicrease_dashes_len', type=float, default=1.0, help='Semicrease dash + gap length.') + self.add_argument('--semicrease_dashes_duty', type=float,default=0.25, help='Semicrease dash duty cycle.') + self.add_argument('--semicrease_dashes_bool', type=inkex.Boolean, default=False, help='Dashed strokes?') + self.add_argument('--semicrease_bool', type=inkex.Boolean, default=True, help='Draw semicreases?') + + # -------------------------------------------------------------------------------------------------------------- + # cut options + self.add_argument('--cut_stroke_color', default=16711935, help='The cut creases color.') + self.add_argument('--cut_stroke_width', type=float, default=0.1, help='Width of cut strokes.') + self.add_argument('--cut_dashes_len', type=float, default=1.0, help='Cut dash + gap length.') + self.add_argument('--cut_dashes_duty', type=float, default=0.25, help='Cut dash duty cycle.') + self.add_argument('--cut_dashes_bool', type=inkex.Boolean, default=False, help='Dashed strokes?') + self.add_argument('--cut_bool', type=inkex.Boolean, default=True, help='Draw cuts?') + + # -------------------------------------------------------------------------------------------------------------- + # vertex options + self.add_argument('--vertex_stroke_color', default=255, help='Vertices\' color.') + self.add_argument('--vertex_stroke_width', type=float, default=0.1, help='Width of vertex strokes.') + self.add_argument('--vertex_radius', type=float, default=0.1, help='Radius of vertices.') + self.add_argument('--vertex_bool', type=bool, default=True, help='Draw vertices?') + # here so we can have tabs - but we do not use it directly - else error + self.add_argument('--active-tab', default='title', help="Active tab.") + + self.path_tree = [] + self.edge_points = [] + self.translate = (0, 0) + + def effect(self): + """ Main function, called when the extension is run. + """ + # construct dictionary containing styles + self.create_styles_dict() + + # get paths for selected origami pattern + self.generate_path_tree() + + # ~ accuracy = self.options.accuracy + # ~ unit_factor = self.calc_unit_factor() + # what page are we on + # page_id = self.options.active_tab # sometimes wrong the very first time + + # Translate according to translate attribute + g_attribs = {inkex.addNS('label', 'inkscape'): '{} Origami pattern'.format(self.options.pattern), + # inkex.addNS('transform-center-x','inkscape'): str(-bbox_center[0]), + # inkex.addNS('transform-center-y','inkscape'): str(-bbox_center[1]), + inkex.addNS('transform-center-x', 'inkscape'): str(0), + inkex.addNS('transform-center-y', 'inkscape'): str(0), + 'transform': 'translate(%s,%s)' % self.translate} + + # add the group to the document's current layer + if type(self.path_tree) == list and len(self.path_tree) != 1: + self.topgroup = etree.SubElement(self.get_layer(), 'g', g_attribs) + else: + self.topgroup = self.get_layer() + + if len(self.edge_points) == 0: + Path.draw_paths_recursively(self.path_tree, self.topgroup, self.styles_dict) + elif self.options.edge_single_path: + edges = Path(self.edge_points, 'e', closed=True) + Path.draw_paths_recursively(self.path_tree + [edges], self.topgroup, self.styles_dict) + else: + edges = Path.generate_separated_paths(self.edge_points, 'e', closed=True) + Path.draw_paths_recursively(self.path_tree + edges, self.topgroup, self.styles_dict) + + # self.draw_paths_recursively(self.path_tree, self.topgroup, self.styles_dict) + + # compatibility hack + def get_layer(self): + try: + return self.svg.get_current_layer() # new + except: + return self.current_layer # old + + def create_styles_dict(self): + """ Get stroke style parameters and use them to create the styles dictionary, used for the Path generation + """ + unit_factor = self.calc_unit_factor() + + # define colour and stroke width + mountain_style = {'draw': self.options.mountain_bool, + 'stroke': self.get_color_string(self.options.mountain_stroke_color), + 'fill': 'none', + 'stroke-width': self.options.mountain_stroke_width*unit_factor} + + valley_style = {'draw': self.options.valley_bool, + 'stroke': self.get_color_string(self.options.valley_stroke_color), + 'fill': 'none', + 'stroke-width': self.options.valley_stroke_width*unit_factor} + + universal_style = {'draw': self.options.universal_bool, + 'stroke': self.get_color_string(self.options.universal_stroke_color), + 'fill': 'none', + 'stroke-width': self.options.universal_stroke_width*unit_factor} + + semicrease_style = {'draw': self.options.semicrease_bool, + 'stroke': self.get_color_string(self.options.semicrease_stroke_color), + 'fill': 'none', + 'stroke-width': self.options.semicrease_stroke_width*unit_factor} + + cut_style = {'draw': self.options.cut_bool, + 'stroke': self.get_color_string(self.options.cut_stroke_color), + 'fill': 'none', + 'stroke-width': self.options.cut_stroke_width*unit_factor} + + edge_style = {'draw': self.options.edge_bool, + 'stroke': self.get_color_string(self.options.edge_stroke_color), + 'fill': 'none', + 'stroke-width': self.options.edge_stroke_width*unit_factor} + + vertex_style = {'draw': self.options.vertex_bool, + 'stroke': self.get_color_string(self.options.vertex_stroke_color), + 'fill': 'none', + 'stroke-width': self.options.vertex_stroke_width*unit_factor} + + # check if dashed option selected + if self.options.mountain_dashes_bool: + dash = self.options.mountain_dashes_len*self.options.mountain_dashes_duty*unit_factor + gap = abs(dash - self.options.mountain_dashes_len*unit_factor) + mountain_style['stroke-dasharray'] = "{},{}".format(dash, gap) + if self.options.valley_dashes_bool: + dash = self.options.valley_dashes_len * self.options.valley_dashes_duty*unit_factor + gap = abs(dash - self.options.valley_dashes_len*unit_factor) + valley_style['stroke-dasharray'] = "{},{}".format(dash, gap) + if self.options.edge_dashes_bool: + dash = self.options.edge_dashes_len * self.options.edge_dashes_duty*unit_factor + gap = abs(dash - self.options.edge_dashes_len*unit_factor) + edge_style['stroke-dasharray'] = "{},{}".format(dash, gap) + if self.options.universal_dashes_bool: + dash = self.options.universal_dashes_len * self.options.universal_dashes_duty*unit_factor + gap = abs(dash - self.options.universal_dashes_len*unit_factor) + universal_style['stroke-dasharray'] = "{},{}".format(dash, gap) + if self.options.semicrease_dashes_bool: + dash = self.options.semicrease_dashes_len * self.options.semicrease_dashes_duty*unit_factor + gap = abs(dash - self.options.semicrease_dashes_len*unit_factor) + semicrease_style['stroke-dasharray'] = "{},{}".format(dash, gap) + if self.options.cut_dashes_bool: + dash = self.options.cut_dashes_len * self.options.cut_dashes_duty*unit_factor + gap = abs(dash - self.options.cut_dashes_len*unit_factor) + cut_style['stroke-dasharray'] = "{},{}".format(dash, gap) + + self.styles_dict = {'m': mountain_style, + 'v': valley_style, + 'u': universal_style, + 's': semicrease_style, + 'c': cut_style, + 'e': edge_style, + 'p': vertex_style} + + def get_color_string(self, longColor, verbose=False): + """ Convert the long into a #RRGGBB color value + - verbose=true pops up value for us in defaults + conversion back is A + B*256^1 + G*256^2 + R*256^3 + """ + # compatibility hack, no "long" in Python 3 + try: + longColor = long(longColor) + if longColor < 0: longColor = long(longColor) & 0xFFFFFFFF + hexColor = hex(longColor)[2:-3] + except: + longColor = int(longColor) + hexColor = hex(longColor)[2:-2] + inkex.debug = inkex.utils.debug + + hexColor = '#' + hexColor.rjust(6, '0').upper() + if verbose: inkex.debug("longColor = {}, hex = {}".format(longColor,hexColor)) + + return hexColor + + def add_text(self, node, text, position, text_height=12): + """ Create and insert a single line of text into the svg under node. + """ + line_style = {'font-size': '%dpx' % text_height, 'font-style':'normal', 'font-weight': 'normal', + 'fill': '#F6921E', 'font-family': 'Bitstream Vera Sans,sans-serif', + 'text-anchor': 'middle', 'text-align': 'center'} + line_attribs = {inkex.addNS('label','inkscape'): 'Annotation', + 'style': str(Inkex.style(line_style)), + 'x': str(position[0]), + 'y': str((position[1] + text_height) * 1.2) + } + line = etree.SubElement(node, inkex.addNS('text','svg'), line_attribs) + line.text = text + + + def calc_unit_factor(self): + """ Return the scale factor for all dimension conversions. + + - The document units are always irrelevant as + everything in inkscape is expected to be in 90dpi pixel units + """ + # namedView = self.document.getroot().find(inkex.addNS('namedview', 'sodipodi')) + # doc_units = self.getUnittouu(str(1.0) + namedView.get(inkex.addNS('document-units', 'inkscape'))) + # backwards compatibility + try: + return self.svg.unittouu(str(1.0) + self.options.units) + except: + try: + return inkex.unittouu(str(1.0) + self.options.units) + except AttributeError: + return self.unittouu(str(1.0) + self.options.units) \ No newline at end of file diff --git a/OrigamiPatterns/Pleat_Circular.py b/OrigamiPatterns/Pleat_Circular.py new file mode 100644 index 00000000..e7103f27 --- /dev/null +++ b/OrigamiPatterns/Pleat_Circular.py @@ -0,0 +1,91 @@ +#! /usr/bin/env python3 + +import inkex +import numpy as np +from math import pi, sin, cos +from Path import Path +from Pattern import Pattern + + +# Select name of class, inherits from Pattern +# TODO: +# 1) Implement __init__ method to get all custom options and then call Pattern's __init__ +# 2) Implement generate_path_tree to define all of the desired strokes + +class PleatCircular(Pattern): + def __init__(self): + Pattern.__init__(self) + self.add_argument("-p", "--pattern", default="pleat_circular", help="Origami pattern") + self.add_argument("--radius", type=float, default=55.0, help="Radius of circle") + self.add_argument("--ratio", type=float, default=0.4, help="Opening ratio") + self.add_argument("--rings", type=int, default=15, help="Number of rings") + self.add_argument("--simulation_mode", type=inkex.Boolean, default=True, help="Approximate circle and draw semicreases for simulation?") + self.add_argument("--sides", type=int, default=20, help="Number of sides for polygon approximating half circle") + + def generate_path_tree(self): + """ Specialized path generation for your origami pattern + """ + # retrieve saved parameters + unit_factor = self.calc_unit_factor() + R = self.options.radius * unit_factor + ratio = self.options.ratio + r = R * ratio + rings = self.options.rings + dr = (1.-ratio)*R/rings + self.translate = (R, R) + + if not self.options.simulation_mode: + inner_circles = [] + for i in range(1, rings): + inner_circles.append(Path((0, 0), radius=r + i*dr, style='m' if i % 2 else 'v')) + + edges = [Path((0, 0), radius=R, style='e'), + Path((0, 0), radius=r, style='e')] + + self.path_tree = [inner_circles, edges] + + # append semicreases for simulation + else: + sides = self.options.sides + dtheta = pi / sides + # create diagonals + diagonals = [] + for i in range(sides): + p1 = (0, 0) + p2 = (R * cos((1 + i * 2) * dtheta), R * sin((1 + i * 2) * dtheta)) + diagonals.append(Path([p1, p2], 'u')) + + s = sin(dtheta) + c = cos(dtheta) + + # Edge + paths = [Path([(c * R, -s * R), (R, 0), (c * R, s * R)], style='e'), + Path([(c * r, -s * r), (r, 0), (c * r, s * r)], style='e')] + + # MV circles + for i in range(1, rings): + r_i = r + i * dr + paths.append(Path([(c * r_i, -s * r_i), (r_i, 0), (c * r_i, s * r_i)], + style='m' if i % 2 else 'v')) + + # Semicreases + top = [] + bottom = [] + for i in range(rings + 1): + r_i = r + i*dr + top.append((r_i*(1 + (i % 2)*(c-1)), -(i % 2)*s*r_i)) + bottom.append((r_i*(1 + (i % 2)*(c-1)), (i % 2)*s*r_i)) + paths = paths + [Path([(r, 0), (R, 0)], 's'), # straight line 1 + Path([(r*c, r*s), (R*c, R*s)], 's', invert=True), # straight line 2 + Path(top, 's'), # top half of semicrease pattern + Path(bottom, 's')] # bottom half of semicrease pattern + + all_paths = [paths] + for i in range(1, sides): + all_paths.append(Path.list_rotate(all_paths[0], i*2*dtheta)) + + self.path_tree = all_paths + +# Main function, creates an instance of the Class and calls inkex.affect() to draw the origami on inkscape +if __name__ == '__main__': + PleatCircular().run() \ No newline at end of file diff --git a/OrigamiPatterns/Template.py b/OrigamiPatterns/Template.py new file mode 100644 index 00000000..c8305726 --- /dev/null +++ b/OrigamiPatterns/Template.py @@ -0,0 +1,97 @@ +#! /usr/bin/env python3 + +import numpy as np +from math import pi +import inkex +from Path import Path +from Pattern import Pattern + +# Select name of class, inherits from Pattern +# TODO: +# 1) Implement __init__ method to get all custom options and then call Pattern's __init__ +# 2) Implement generate_path_tree to define all of the desired strokes + +class Template(Pattern): + def __init__(self): + Pattern.__init__(self) + self.add_argument('-p', '--pattern', default="template1", help="Origami pattern") + self.add_argument('--length', type=float, default=10.0, help="Length of grid square") + self.add_argument('--theta', type=int, default=0, help="Rotation angle (degree)") + + def generate_path_tree(self): + """ Specialized path generation for your origami pattern + """ + # retrieve conversion factor for selected unit + unit_factor = self.calc_unit_factor() + + # retrieve saved parameters, and apply unit factor where needed + length = self.options.length * unit_factor + vertex_radius = self.options.vertex_radius * unit_factor + pattern = self.options.pattern + theta = self.options.theta * pi / 180 + + # create all Path instances defining strokes + # first define its points as a list of tuples... + mountain_h_stroke_points = [(length / 2, 0), + (length / 2, length)] + mountain_v_stroke_points = [(0, length / 2), + (length, length / 2)] + + # ... and then create the Path instances, defining its type ('m' for mountain, etc...) + mountains = [Path(mountain_h_stroke_points, 'm' if pattern == 'template1' else 'v'), + Path(mountain_v_stroke_points, 'm' if pattern == 'template1' else 'v')] + + # doing the same for valleys + valley_1st_stroke_points = [(0, 0), + (length, length)] + valley_2nd_stroke_points = [(0, length), + (length, 0)] + valleys = [Path(valley_1st_stroke_points, 'v' if pattern == 'template1' else 'm'), + Path(valley_2nd_stroke_points, 'v' if pattern == 'template1' else 'm')] + + + + vertices = [] + for i in range(3): + for j in range(3): + vertices.append(Path(((i/2.) * length, (j/2.) * length), style='p', radius=vertex_radius)) + + # multiplication is implemented as a rotation, and list_rotate implements rotation for list of Path instances + vertices = Path.list_rotate(vertices, theta, (1 * length, 1 * length)) + mountains = Path.list_rotate(mountains, theta, (1 * length, 1 * length)) + valleys = Path.list_rotate(valleys, theta, (1 * length, 1 * length)) + + # if Path constructor is called with more than two points, a single stroke connecting all of then will be + # created. Using method generate_separated_paths, you can instead return a list of separated strokes + # linking each two points + # create a list for edge strokes + edge_points = [(0 * length, 0 * length), # top left + (1 * length, 0 * length), # top right + (1 * length, 1 * length), # bottom right + (0 * length, 1 * length)] # bottom left + + # create path from points to be able to use the already built rotate method + edges = Path(edge_points, 'e', closed=True) + edges = Path.list_rotate(edges, theta, (1 * length, 1 * length)) + + # division is implemented as a reflection, and list_reflect implements it for a list of Path instances + # here's a commented example: + # line_reflect = (0 * length, 2 * length, 1 * length, 1 * length) + # mountains = Path.list_reflect(mountains, line_reflect) + # valleys = Path.list_reflect(valleys, line_reflect) + # edges = Path.list_reflect(edges, line_reflect) + + # IMPORTANT: at the end, save edge points as "self.edge_points", to simplify selection of single or multiple + # strokes for the edge + self.edge_points = edges.points + + # IMPORTANT: the attribute "path_tree" must be created at the end, saving all strokes + self.path_tree = [mountains, valleys, vertices] + # if you decide not to declare "self.edge_points", then the edge must be explicitly created in the path_tree: + # self.path_tree = [mountains, valleys, vertices, edges] + + +# Main function, creates an instance of the Class and calls self.draw() to draw the origami on inkscape +# self.draw() is either a call to inkex.affect() or to svg.run(), depending on python version +if __name__ == '__main__': + Template().run() \ No newline at end of file diff --git a/OrigamiPatterns/Waterbomb.py b/OrigamiPatterns/Waterbomb.py new file mode 100644 index 00000000..658ced48 --- /dev/null +++ b/OrigamiPatterns/Waterbomb.py @@ -0,0 +1,103 @@ +#! /usr/bin/env python3 + +import math +import numpy as np +import inkex +from Path import Path +from Pattern import Pattern + +# TODO: +# Add fractional column number option + +class Waterbomb(Pattern): + def __init__(self): + Pattern.__init__(self) + self.add_argument("-p", "--pattern", default="waterbomb", help="Origami pattern") + self.add_argument("--pattern_first_line", default="waterbomb", help="Origami pattern") + self.add_argument("--pattern_last_line", default="waterbomb", help="Origami pattern") + self.add_argument("--lines", type=int, default=8, help="Number of lines") + self.add_argument("--columns", type=int, default=16, help="Number of columns") + self.add_argument("--length", type=float, default=10.0, help="Length of grid square") + self.add_argument('--phase_shift', type=inkex.Boolean, default=True, help='Shift phase of tesselation.') + + def generate_path_tree(self): + """ Specialized path generation for Waterbomb tesselation pattern + """ + unit_factor = self.calc_unit_factor() + length = self.options.length * unit_factor + vertex_radius = self.options.vertex_radius * unit_factor + cols = self.options.columns + lines = self.options.lines + phase_shift = self.options.phase_shift + pattern_first_line = self.options.pattern_first_line + pattern_last_line = self.options.pattern_last_line + + # create vertices + vertex_line_types = [[Path(((i / 2.) * length, 0), style='p', radius=vertex_radius) for i in range(2*cols + 1)], + [Path((i * length, 0), style='p', radius=vertex_radius) for i in range(cols + 1)], + [Path(((i + 0.5) * length, 0), style='p', radius=vertex_radius) for i in range(cols)]] + + vertices = [] + for i in range(2*lines + 1): + if i % 2 == 0 or (pattern_first_line == 'magic_ball' and i == 1) or (pattern_last_line == 'magic_ball' and i == 2*lines - 1): + type = 0 + elif(i/2 + phase_shift) % 2 == 0: + type = 1 + else: + type = 2 + vertices = vertices + Path.list_add(vertex_line_types[type], (0, 0.5*i*length)) + + # create a list for the horizontal creases and another for the vertical creases + # alternate strokes to minimize laser cutter path + corr_fist_line = length/2 if pattern_first_line == 'magic_ball' else 0 + corr_last_line = length/2 if pattern_last_line == 'magic_ball' else 0 + grid = [Path.generate_hgrid([0, length*cols], [0, length*lines], lines, 'm'), + Path.generate_vgrid([0, length*cols], [corr_fist_line, length*lines-corr_last_line], 2*cols, 'm')] + + vgrid_a = Path.generate_vgrid([0, length * cols], [0, length / 2], 2 * cols, 'v') + vgrid_b = Path.list_add(vgrid_a, (0, (lines - 0.5) * length)) + if pattern_first_line == 'magic_ball' and pattern_last_line == 'magic_ball': + grid[1] = [[vgrid_a[i], grid[1][i], vgrid_b[i]] if i % 2 == 0 else + [vgrid_b[i], grid[1][i], vgrid_a[i]] for i in range(len(grid[1]))] + elif pattern_first_line == 'magic_ball': + grid[1] = [[vgrid_a[i], grid[1][i]] if i % 2 == 0 else + [grid[1][i], vgrid_a[i]] for i in range(len(grid[1]))] + elif pattern_last_line == 'magic_ball': + grid[1] = [[grid[1][i], vgrid_b[i]] if i % 2 == 0 else + [vgrid_b[i], grid[1][i]] for i in range(len(grid[1]))] + + # create generic valley Path lines, one pointing up and other pointing down + valley_types = [Path([(i * length / 2, (1 - i % 2) * length / 2) for i in range(2 * cols + 1)], 'v'), + Path([( i*length/2, (i % 2)*length/2) for i in range(2 * cols + 1)], 'v')] + + # define which lines must be of which type, according to parity and options + senses = np.array([bool((i % 2+i)/2 % 2) for i in range(2*lines)]) + senses = 1*senses # converts bool array to 0's and 1's + if phase_shift: + senses = np.invert(senses) + if pattern_first_line == "magic_ball": + senses[0] = ~senses[0] + if pattern_last_line == "magic_ball": + senses[-1] = ~senses[-1] + valleys = [valley_types[senses[i]] + (0, i * length / 2) for i in range(2*lines)] + + # convert first and last lines to mountains if magic_ball + if pattern_first_line == "magic_ball": + valleys[0].style = 'm' + if pattern_last_line == "magic_ball": + valleys[-1].style = 'm' + + # invert every two lines to minimize laser cutter movements + for i in range(1, 2*lines, 2): + valleys[i].invert() + + self.edge_points = [(0*length*cols, 0*length*lines), # top left + (1*length*cols, 0*length*lines), # top right + (1*length*cols, 1*length*lines), # bottom right + (0*length*cols, 1*length*lines)] # bottom left + + self.path_tree = [grid, valleys, vertices] + + +if __name__ == '__main__': + Waterbomb().run() \ No newline at end of file diff --git a/OrigamiPatterns/__init__.py b/OrigamiPatterns/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fablabchemnitz_origami_patterns_kresling.inx b/fablabchemnitz_origami_patterns_kresling.inx new file mode 100644 index 00000000..f8eaa1ca --- /dev/null +++ b/fablabchemnitz_origami_patterns_kresling.inx @@ -0,0 +1,92 @@ + + + <_name>Origami Pattern - Kresling tower + fablabchemnitz.de.origami_patterns.kresling_full + + + + <_option value="regular">Regular + <_option value="mirrowed">Mirror odd cells + + <_param name="help" type="description" xml:space="preserve">------------------------------------------------------------ + 3 + 6 + <_param name="help" type="description" xml:space="preserve">------------------------------------------------------------ + 10.0 + + + + + + + + + + + + + + + + + <_param name="help" type="description" xml:space="preserve">------------------------------------------------------------ + + + + + 0.5 + 0.5 + 60.0 + + + + false + 100 + <_param name="help" type="description" xml:space="preserve">------------------------------------------------------------ + + + true + true + 1 + 0.5 + 0.1 + 4278190335 + + + true + true + 1 + 0.25 + 0.1 + 65535 + + + true + true + false + 1 + 0.25 + 0.1 + 255 + + + false + 0.1 + 0.1 + 255 + + + + all + + + + + + + + \ No newline at end of file diff --git a/fablabchemnitz_origami_patterns_pleat_circular.inx b/fablabchemnitz_origami_patterns_pleat_circular.inx new file mode 100644 index 00000000..4a2eea9e --- /dev/null +++ b/fablabchemnitz_origami_patterns_pleat_circular.inx @@ -0,0 +1,67 @@ + + + <_name>Origami Pattern - Circular + fablabchemnitz.de.origami_patterns.pleat_circular + + + 55.0 + + + + + + + 0.4 + 15 + <_param name="help" type="description" xml:space="preserve">------------------------------ + true + <_param name="help" type="description" xml:space="preserve">To simulate with OrigamiSimulator, semicreases (or facet creases) must be added to properly simulate paper, and the circles must be approximated as polygons. + 20 + + + true + true + 1 + 0.5 + 0.1 + 4278190335 + + + true + true + 1 + 0.25 + 0.1 + 65535 + + + true + true + false + 1 + 0.25 + 0.1 + 255 + + + true + false + 1 + 0.25 + 0.1 + 4294902015 + + + + + all + + + + + + + + \ No newline at end of file diff --git a/fablabchemnitz_origami_patterns_pleat_hypar.inx b/fablabchemnitz_origami_patterns_pleat_hypar.inx new file mode 100644 index 00000000..8b011ec8 --- /dev/null +++ b/fablabchemnitz_origami_patterns_pleat_hypar.inx @@ -0,0 +1,78 @@ + + + <_name>Origami Pattern - N-sided Hypar + fablabchemnitz.de.origami_patterns.pleat_hypar + + + + <_option value="classic">Classic Hypar + <_option value="asymmetric">Asymmetric triangulation + <_option value="alternate_asymmetric">Alternating asymmetric triangulation + + 100.0 + + + + + + + 4 + 7 + false + <_param name="help" type="description" xml:space="preserve">Implements Hypar (classical hyperbolic paraboloid approximate). Classic Hypar is the easiest one to fold. However, it's not rigid foldable. More information in: Demaine, E. D., Demaine, M. L., Hart, V., Price, G. N., & Tachi, T. (2011). (Non)Existence of Pleated Folds: How Paper Folds Between Creases. Graphs and Combinatorics, 27(3), 377–397. https://doi.org/10.1007/s00373-011-1025-2 + + + + true + true + 1 + 0.5 + 0.1 + 4278190335 + + + true + true + 1 + 0.25 + 0.1 + 65535 + + + true + false + 1 + 0.25 + 0.1 + 4278255615 + + + true + true + false + 1 + 0.25 + 0.1 + 255 + + + false + 0.1 + 0.1 + 255 + + + + + all + + + + + + + + + \ No newline at end of file diff --git a/fablabchemnitz_origami_patterns_template.inx b/fablabchemnitz_origami_patterns_template.inx new file mode 100644 index 00000000..19e30b3d --- /dev/null +++ b/fablabchemnitz_origami_patterns_template.inx @@ -0,0 +1,90 @@ + + + <_name>Origami Pattern - Template effect + fablabchemnitz.de.origami_patterns.template + + + + <_option value="template1">Template pattern 1 + <_option value="template2">Template pattern 2 + + 10.0 + + + + + + + 0 + <_param name="help" type="description" xml:space="preserve">The .inx file defines the bridge between Inkscape's interface and the python script. + + + + true + true + 1 + 0.5 + 0.1 + 4278190335 + + + true + true + 1 + 0.25 + 0.1 + 65535 + + + true + true + false + 1 + 0.25 + 0.1 + 255 + + + true + 0.1 + 0.1 + 255 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + + + + + + + + \ No newline at end of file diff --git a/fablabchemnitz_origami_patterns_waterbomb.inx b/fablabchemnitz_origami_patterns_waterbomb.inx new file mode 100644 index 00000000..f37123e0 --- /dev/null +++ b/fablabchemnitz_origami_patterns_waterbomb.inx @@ -0,0 +1,73 @@ + + + <_name>Origami Pattern - Waterbomb + fablabchemnitz.de.origami_patterns.magic_ball + + + + <_option value="waterbomb">Regular waterbomb + <_option value="magic_ball">Magic ball + + + <_option value="waterbomb">Regular waterbomb + <_option value="magic_ball">Magic ball + + false + <_param name="help" type="description" xml:space="preserve">------------------------------ + 8 + 16 + <_param name="help" type="description" xml:space="preserve">------------------------------ + 10.0 + + + + + + + <_param name="help" type="description" xml:space="preserve">"Waterbomb tessellation" creates a simple tessellation pattern repeating the Waterbomb base, with a half-step phase shift between each line. The Magic ball is a different design that inverts both the upper half of the first line and the bottom half of the last line. + + + + true + true + 1 + 0.5 + 0.1 + 4278190335 + + + true + true + 1 + 0.25 + 0.1 + 65535 + + + true + true + false + 1 + 0.25 + 0.1 + 255 + + + false + 0.1 + 0.1 + 255 + + + + all + + + + + + + + \ No newline at end of file