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 @@
+
+
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