diff --git a/extensions/fablabchemnitz/buxtronix_living_hinge/images/about.png b/extensions/fablabchemnitz/buxtronix_living_hinge/images/about.png new file mode 100644 index 00000000..b4bfd54a Binary files /dev/null and b/extensions/fablabchemnitz/buxtronix_living_hinge/images/about.png differ diff --git a/extensions/fablabchemnitz/buxtronix_living_hinge/images/cross-lattice.png b/extensions/fablabchemnitz/buxtronix_living_hinge/images/cross-lattice.png new file mode 100644 index 00000000..41264d6f Binary files /dev/null and b/extensions/fablabchemnitz/buxtronix_living_hinge/images/cross-lattice.png differ diff --git a/extensions/fablabchemnitz/buxtronix_living_hinge/images/diamond-lattice.png b/extensions/fablabchemnitz/buxtronix_living_hinge/images/diamond-lattice.png new file mode 100644 index 00000000..d03cf75b Binary files /dev/null and b/extensions/fablabchemnitz/buxtronix_living_hinge/images/diamond-lattice.png differ diff --git a/extensions/fablabchemnitz/buxtronix_living_hinge/images/diamond-lattice.svg b/extensions/fablabchemnitz/buxtronix_living_hinge/images/diamond-lattice.svg new file mode 100644 index 00000000..2d738b9a --- /dev/null +++ b/extensions/fablabchemnitz/buxtronix_living_hinge/images/diamond-lattice.svg @@ -0,0 +1,143 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/fablabchemnitz/buxtronix_living_hinge/images/straight-lattice.png b/extensions/fablabchemnitz/buxtronix_living_hinge/images/straight-lattice.png new file mode 100644 index 00000000..dd911d59 Binary files /dev/null and b/extensions/fablabchemnitz/buxtronix_living_hinge/images/straight-lattice.png differ diff --git a/extensions/fablabchemnitz/buxtronix_living_hinge/images/wavy-lattice.png b/extensions/fablabchemnitz/buxtronix_living_hinge/images/wavy-lattice.png new file mode 100644 index 00000000..d3a0d94b Binary files /dev/null and b/extensions/fablabchemnitz/buxtronix_living_hinge/images/wavy-lattice.png differ diff --git a/extensions/fablabchemnitz/buxtronix_living_hinge/living_hinge.inx b/extensions/fablabchemnitz/buxtronix_living_hinge/living_hinge.inx new file mode 100644 index 00000000..a42f2106 --- /dev/null +++ b/extensions/fablabchemnitz/buxtronix_living_hinge/living_hinge.inx @@ -0,0 +1,68 @@ + + + Buxtronix Living Hinges + fablabchemnitz.de.buxtronix_living_hinges + + + images/straight-lattice.png + 20 + 2.0 + 0.0 + + + + images/diamond-lattice.png + 20 + 4.0 + 0.5 + + + + images/cross-lattice.png + 20 + 6.0 + + + + images/wavy-lattice.png + 29 + 4.0 + + + + + images/about.png + + + + 100 + 100 + + + + + + + + false + + all + + + + + + + + diff --git a/extensions/fablabchemnitz/buxtronix_living_hinge/living_hinge.py b/extensions/fablabchemnitz/buxtronix_living_hinge/living_hinge.py new file mode 100755 index 00000000..637bcf44 --- /dev/null +++ b/extensions/fablabchemnitz/buxtronix_living_hinge/living_hinge.py @@ -0,0 +1,450 @@ +#!/usr/bin/env python3 + +import inkex +cut_colour = '#ff0000' +engrave_colour = '#0000ff' + +class Generator(object): + """A generic generator, subclassed for each different lattice style.""" + + def __init__(self, x, y, width, height, stroke_width, svg, e_length, p_spacing): + self.x = x + self.y = y + self.width = width + self.height = height + self.stroke_width = stroke_width + self.svg = svg + self.canvas = self.svg.get_current_layer() + self.e_length = e_length + self.e_height = 0 # Provided by sub-classes. + self.p_spacing = p_spacing + self.fixed_commands = "" + + def draw_one(self, x, y): + return "M %f,%f %s" % (x, y, self.fixed_commands) + + def parameter_text(self): + return "length: %.f spacing: %.1f" % ( + self.e_length, + self.p_spacing, + ) + + def draw_swatch(self): + border = self.canvas.add(inkex.PathElement()) + # Curve radius + cr = self.svg.unittouu('10mm') + # Swatch padding + sp = self.svg.unittouu('30mm') + # Handle length + hl = cr/2 + path_command = ( + 'm %f,%f l %f,%f c %f,%f %f,%f %f,%f' + 'l %f,%f c %f,%f %f,%f %f,%f ' + 'l %f,%f c %f,%f %f,%f %f,%f ' + 'l %f,%f c %f,%f %f,%f %f,%f ') % ( + cr, 0, + + self.width - 2*cr, 0, + + hl, 0, + cr, cr-hl, + cr, cr, + + 0, self.height - 2*cr + 1.5*sp, + + 0, cr/2, + 0-cr+hl, cr, + 0-cr, cr, + + 0-self.width + 2*cr, 0, + + 0-hl, 0, + 0-cr, 0-cr+hl, + 0-cr, 0-cr, + + 0, 0-self.height - 1.5*sp + 2*cr, + + 0, 0-hl, + cr-hl, 0-cr, + cr, 0-cr + ) + + style = { + "stroke": cut_colour, + "stroke-width": str(self.stroke_width), + "fill": "none", + } + border.update(**{"style": style, "inkscape:label": "lattice_border", "d": path_command}) + + c = self.canvas.add(inkex.Circle( + style=str(inkex.Style(style)), + cx=str(cr), + cy=str(cr), + r=str(self.svg.unittouu('4mm')))) + + self.y += sp + + text_style = { + 'fill': engrave_colour, + 'font-size': '9px', + 'font-family': 'sans-serif', + 'text-anchor': 'middle', + 'text-align': 'center', + } + text = self.canvas.add( + inkex.TextElement( + style=str(inkex.Style(text_style)), + x=str(self.x + self.width/2), + y=str(self.y - sp/2))) + text.text = "Style: %s" % self.name + + text_style['font-size'] = "3px" + text = self.canvas.add( + inkex.TextElement( + style=str(inkex.Style(text_style)), + x=str(self.x + self.width/2), + y=str(self.y - sp/4))) + text.text = self.parameter_text() + + text = self.canvas.add( + inkex.TextElement( + style=str(inkex.Style(text_style)), + x=str(self.x + self.width/2), + y=str(self.y +self.height + sp/4))) + text.text = "https://github.com/buxtronix/living-hinge" + + def generate(self, swatch): + if swatch: + self.draw_swatch() + # Round width/height to integer number of patterns. + self.e_length = self.width / max(round(self.width / self.e_length), 1.0) + self.e_height = self.height / max(round(self.height / self.e_height), 1.0) + self.prerender() + style = { + "stroke": cut_colour, + "stroke-width": str(self.stroke_width), + "fill": "none", + } + path_command = "" + y = self.y + while y < self.y + self.height: + x = self.x + while x < self.x + self.width: + path_command = "%s %s " % (path_command, self.draw_one(x, y)) + x += self.e_length + y += self.e_height + + link = self.canvas.add(inkex.PathElement()) + link.update(**{"style": style, "inkscape:label": "lattice", "d": path_command}) + link.description("%s hinge %s" % (self.name, self.parameter_text())) + + +class StraightLatticeGenerator(Generator): + def __init__(self, *args, **kwargs): + super(StraightLatticeGenerator, self).__init__(*args) + self.link_gap = kwargs['link_gap'] + self.e_height = 2 * self.p_spacing + self.name = "straight" + + def prerender(self): + self.e_height = 2 * self.p_spacing + w = self.e_length + lg = self.link_gap + + if lg < 0.1: + # Single line for 0 height gap. + self.fixed_commands = " m %f,%f h %f m %f,%f h %f m %f,%f h %f" % ( + 0, self.e_height / 2, + w * 2 / 5, + + 0 - w / 5, 0 - self.e_height / 2, + w * 3 / 5, + + 0 - w / 5, self.e_height / 2, + w * 2 / 5, + ) + else: + self.fixed_commands = ( + " m %f,%f h %f v %f h %f" + " m %f,%f h %f v %f h %f v %f" + " m %f,%f h %f v %f h %f " + ) % ( + 0, + self.e_height / 2, + w * 2 / 5, + lg, + 0 - w * 2 / 5, + w / 8, + 0 - lg - self.e_height / 2, + w * 3 / 4, + lg, + 0 - w * 3 / 4, + 0 - lg, + w * 7 / 8, + lg + self.e_height / 2, + 0 - w * 2 / 5, + 0 - lg, + w * 2 / 5, + ) + + def parameter_text(self): + text = super(StraightLatticeGenerator, self).parameter_text() + return "%s element_height: %.1f" % (text, self.link_gap) + + +class DiamondLatticeGenerator(Generator): + def __init__(self, *args, **kwargs): + super(DiamondLatticeGenerator, self).__init__(*args) + self.e_height = self.p_spacing + self.diamond_curve = kwargs['diamond_curve'] + self.name = "diamond" + + def prerender(self): + h = self.e_height + w = self.e_length + # Diamond curve + dc = 0-self.diamond_curve + # Horiz handle length. + hhl = abs(dc * w * 0.2) + # Endpoint horiz handle length + ehhl = hhl if dc > 0 else 0 + # Vert handle length + vhl = abs(dc * h / 8) if dc < 0 else 0 + # Left + self.fixed_commands = " m %f,%f c %f,%f %f,%f %f,%f c %f,%f %f,%f %f,%f " % ( + 0, h / 4, + + hhl, 0, + w * 0.4 - ehhl, h / 4 - vhl, + w * 0.4, h / 4, + + 0 - ehhl, vhl, + 0 - (w * 0.4 - hhl), h / 4, + 0 - w * 0.4, h / 4, + ) + + # Bottom + self.fixed_commands = "%s m %f,%f c %f,%f %f,%f %f,%f s %f,%f %f,%f " % ( + self.fixed_commands, + w * 0.1, h / 4, + + ehhl, 0 - vhl, + w * 0.4 - hhl, 0 - h / 4, + w * 0.4, 0 - h / 4, + + w * 0.4 - ehhl, h / 4 - vhl, + w * 0.4, h / 4, + ) + + # Top + self.fixed_commands = "%s m %f,%f c %f,%f %f,%f %f,%f s %f,%f %f,%f " % ( + self.fixed_commands, + 0 - w * 0.8, 0 - h, + + ehhl, vhl, + w * 0.4 - hhl, h / 4, + w * 0.4, h / 4, + + w * 0.4 - ehhl, 0 - h / 4 + vhl, + w * 0.4, 0 - h / 4, + ) + + # Right + self.fixed_commands = "%s m %f,%f c %f,%f %f,%f %f,%f c %f,%f %f,%f %f,%f " % ( + self.fixed_commands, + w * 0.1, h *0.75, + + 0 - hhl, 0, + (0 - w * 0.4) + ehhl, 0 - h / 4 + vhl, + 0 - w * 0.4, 0 - h / 4, + + ehhl, 0 - vhl, + w * 0.4 - hhl, 0 - h / 4, + w * 0.4, 0 - h / 4, + ) + + def draw_one(self, x, y): + return "M %f,%f %s" % (x, y, self.fixed_commands) + + def parameter_text(self): + text = super(DiamondLatticeGenerator, self).parameter_text() + return "%s curve: %.1f" % (text, self.diamond_curve) + + +class CrossLatticeGenerator(Generator): + def __init__(self, *args): + super(CrossLatticeGenerator, self).__init__(*args) + self.e_height = self.p_spacing + self.name = "cross" + + def prerender(self): + l = self.e_length + h = self.e_height + self.fixed_commands = ( + "m %f,%f l %f,%f l %f,%f m %f,%f l %f,%f" + "m %f,%f l %f,%f l %f,%f l %f,%f " + "m %f,%f l %f,%f l %f,%f l %f,%f " + "m %f,%f l %f,%f l %f,%f m %f,%f l %f,%f" + ) % ( + # Left + 0, h * 0.5, + l * 0.2, 0, + l * 0.2, 0 - h * 0.3, + 0 - l * 0.2, h * 0.3, + l * 0.2, h * 0.3, + # Top + 0 - l * 0.3, 0 - h * 0.5, + l * 0.2, 0 - h * 0.3, + l * 0.4, 0, + l * 0.2, h * 0.3, + # Bottom + 0, h * 0.4, + 0 - l * 0.2, h * 0.3, + 0 - l * 0.4, 0, + 0 - l * 0.2, 0 - h * 0.3, + # Right + l * 0.5, 0 - h * 0.5, + l * 0.2, h * 0.3, + 0 - l * 0.2, h * 0.3, + l * 0.2, 0 - h * 0.3, + l * 0.2, 0, + ) + + +class WavyLatticeGenerator(Generator): + def __init__(self, *args, **kwargs): + super(WavyLatticeGenerator, self).__init__(*args) + self.e_height = self.p_spacing + self.name = "wavy" + + def prerender(self): + h = self.e_height + w = self.e_length + self.fixed_commands = ( + " m %f,%f h %f c %f,%f %f,%f %f,%f h %f " + "m %f,%f h %f c %f,%f %f,%f %f,%f h %f " + ) % ( + 0, h, # Start of element (left) + w * 0.1, # Short horiz line. + + w * 0.1, 0, # Control 1 + w * 3 / 40, 0 - h / 2, # Control 2 + w * 0.2, 0 - h / 2, # Curve top. + + w * 0.175, # Top horiz line. + + 0 - w * 0.1, 0 - h / 2, # Move to higher line. + w * 0.3, # Long higher horiz line. + + w / 5, 0, # Control 1 + w / 10, h, # Control 2 + w * 0.25, h, # Curve down. + + w * 0.075, # End horiz line. + ) + + +class LivingHingeEffect(inkex.EffectExtension): + """ + Extension to create laser cut bend lattices. + """ + + def add_arguments(self, pars): + pars.add_argument("--tab", help="Bend pattern to generate") + pars.add_argument("--unit", help="Units for dimensions") + pars.add_argument("--swatch", type=inkex.Boolean, help="Draw as a swatch card") + + pars.add_argument("--width", type=float, default=300, help="Width of pattern") + pars.add_argument("--height", type=float, default=100, help="Height of pattern") + + pars.add_argument("--sl_length", type=int, default=20, help="Length of links") + pars.add_argument("--sl_gap", type=float, default=0.5, help="Gap between links") + pars.add_argument("--sl_spacing", type=float, default=20, help="Spacing of links") + + pars.add_argument("--dl_curve", type=float, default=0.5, help="Curve of diamonds") + pars.add_argument("--dl_length", type=float, default=24, help="Length of diamonds") + pars.add_argument("--dl_spacing", type=float, default=4, help="Spacing of diamonds") + + pars.add_argument("--cl_length", type=float, default=24, help="Length of combs") + pars.add_argument("--cl_spacing", type=float, default=6, help="Spacing of combs") + + pars.add_argument("--wl_length", type=int, default=20, help="Length of links") + pars.add_argument("--wl_interval", type=int, default=30, help="Interval between links") + pars.add_argument("--wl_spacing", type=float, default=0.5, help="Spacing between links") + + def convert(self, value): + return self.svg.unittouu(str(value) + self.options.unit) + + def convertmm(self, value): + return self.svg.unittouu('%fmm' % value) + + def effect(self): + stroke_width = self.svg.unittouu("0.2mm") + self.options.width = self.convert(self.options.width) + self.options.height = self.convert(self.options.height) + + def draw_one(x, y): + if self.options.tab == "straight_lattice": + generator = StraightLatticeGenerator( + x, + y, + self.options.width, + self.options.height, + stroke_width, + self.svg, + self.convertmm(self.options.sl_length), + self.convertmm(self.options.sl_spacing), + link_gap=self.convertmm(self.options.sl_gap), + ) + elif self.options.tab == "diamond_lattice": + generator = DiamondLatticeGenerator( + x, + y, + self.options.width, + self.options.height, + stroke_width, + self.svg, + self.convertmm(self.options.dl_length), + self.convertmm(self.options.dl_spacing), + diamond_curve=self.options.dl_curve, + ) + elif self.options.tab == "cross_lattice": + generator = CrossLatticeGenerator( + x, + y, + self.options.width, + self.options.height, + stroke_width, + self.svg, + self.convertmm(self.options.cl_length), + self.convertmm(self.options.cl_spacing), + ) + elif self.options.tab == "wavy_lattice": + generator = WavyLatticeGenerator( + x, + y, + self.options.width, + self.options.height, + stroke_width, + self.svg, + self.convertmm(self.options.wl_length), + self.convertmm(self.options.wl_spacing), + ) + else: + inkex.errormsg("Select a valid pattern tab before rendering.") + return + generator.generate(self.options.swatch) + + if self.options.swatch or not self.svg.selected: + draw_one(0, 0) + else: + for elem in self.svg.selected.values(): + # Determine width and height based on the selected object's bounding box. + bbox = elem.bounding_box() + self.options.width = bbox.width + self.options.height = bbox.height + x = bbox.x.minimum + y = bbox.y.minimum + draw_one(x, y) + +LivingHingeEffect().run() \ No newline at end of file