--- /dev/null
+*.pyc
+hexaflexagon-template.svg
--- /dev/null
+all:
+
+install_gimp_plugin:
+ ln -sf $(PWD)/src/gimp_hexaflexagon.py ~/.gimp-2.8/plug-ins/
+ ln -sf $(PWD)/src/gimp_diagram_test.py ~/.gimp-2.8/plug-ins/
+
+uninstall_gimp_plugin:
+ rm -f ~/.gimp-2.8/plug-ins/gimp_hexaflexagon.py
+ rm -f ~/.gimp-2.8/plug-ins/gimp_diagram_test.py
+
+pep8:
+ find . -name "*.py" -print0 | xargs -r0 pep8 --ignore=E501
+
+clean:
+ find . -name "*.pyc" -print0 | xargs -r0 rm
+ find . -type d -name "__pycache__" -print0 | xargs -r0 rm -r
--- /dev/null
+# flexagon-toolkit
+
+flexagon-toolkit is a collection of tools to make it easier to build flexagons.
+
+
+## Draw a template
+
+To draw a template for a hexaflexagon execute:
+
+```
+$ ./src/cairo_hexaflexagon_template.py
+```
+
+And check out the generated `hexaflexagon-template.svg' file.
+
+
+## Gimp plugin
+
+To play with the GIMP plugin, install it with the following command:
+
+```
+$ make install_gimp_plugin
+```
+
+Then launch Gimp and go to `Filters -> Render -> Hexaflexagon`.
+
+
+## Live SVG hexaflexagon editing in Inkscape
+
+Create a new base file with the following command:
+
+```
+$ ./src/svg_hexaflexagon_editor.py
+```
+
+Open the generated file `inkscape-hexaflexagon-editor.svg` in Inkscape and add
+the hexagon content to the layers named *Hexagon 1*, *Hexagon 2*, *Hexagon3*.
+
+See the hexaflexagon being composed automatically and interactively.
--- /dev/null
+- Draw also a folding template for the flexagon.
+- Complete the implementation of the diagram backends.
+- Revisit the design of the Diagram abstraction, and maybe split it to
+ a separate project.
--- /dev/null
+#!/usr/bin/env python
+
+from affine import Affine
+from math import sin, cos, radians
+
+src_x = 10
+src_y = 10
+
+angle = 30
+
+dest_x = 20
+dest_y = 30
+
+# Some transform to compose
+T1 = Affine.translation(-src_x, -src_y)
+R = Affine.rotation(angle)
+T2 = Affine.translation(dest_x, dest_y)
+
+# Composition is performed by multiplying from right to left
+matrix = T2 * R * T1
+print [item for item in matrix]
+
+theta = radians(angle)
+
+# This is the equivalent transformation matrix
+matrix = [
+ cos(theta), -sin(theta), -src_x * cos(theta) + src_y * sin(theta) + dest_x,
+ sin(theta), cos(theta), -src_x * sin(theta) - src_y * cos(theta) + dest_y,
+ 0.0, 0.0, 1.0
+]
+print matrix
--- /dev/null
+#!/usr/bin/env python
+#
+# Draw a hexaflexagon template with cairo.
+#
+# Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from diagram.cairo_diagram import CairoDiagram
+from flexagon import HexaflexagonDiagram
+
+
+def draw_cairo_template():
+ # A4 page at 300dpi is 3508x2480 in pixels but cairo expects units to be in
+ # points, so adjust the values.
+ width = 3508 / 1.25
+ height = 2480 / 1.25
+
+ x_border = width / 50
+ font_size = width / 80
+ stroke_width = width / 480
+
+ cairo_backend = CairoDiagram(width, height, font_size=font_size, stroke_width=stroke_width)
+ hexaflexagon = HexaflexagonDiagram(x_border, backend=cairo_backend)
+
+ cairo_backend.clear()
+ hexaflexagon.draw_template()
+ cairo_backend.save_svg('hexaflexagon-template')
+
+
+if __name__ == '__main__':
+ draw_cairo_template()
--- /dev/null
+#!/usr/bin/env python
+#
+# A Diagram abstraction based on Cairo
+#
+# Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from math import cos, sin, pi
+import cairo
+try:
+ from .diagram import Diagram
+except ValueError:
+ from diagram import Diagram
+
+
+class CairoDiagram(Diagram):
+ def __init__(self, width, height, **kwargs):
+ super(CairoDiagram, self).__init__(width, height, **kwargs)
+
+ self.surface = cairo.RecordingSurface(0, (0, 0, width, height))
+ self.cr = cr = cairo.Context(self.surface)
+
+ cr.select_font_face("Georgia", cairo.FONT_SLANT_NORMAL,
+ cairo.FONT_WEIGHT_NORMAL)
+ cr.set_font_size(self.font_size)
+
+ cr.set_line_width(self.stroke_width)
+ cr.set_line_join(cairo.LINE_JOIN_ROUND)
+
+ def clear(self):
+ cr = self.cr
+
+ r, g, b, a = self.color_to_rgba(self.background)
+ cr.set_source_rgba(r, g, b, a)
+ cr.paint()
+
+ def save_svg(self, filename):
+ surface = cairo.SVGSurface(filename + '.svg', self.width, self.height)
+ # TODO: call surface.set_document_unit() to set units to pixels
+ cr = cairo.Context(surface)
+ cr.set_source_surface(self.surface, 0, 0)
+ cr.paint()
+
+ def save_png(self, filename):
+ surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
+ self.width, self.height)
+ cr = cairo.Context(surface)
+ cr.set_source_surface(self.surface, 0, 0)
+ cr.paint()
+ surface.write_to_png(filename + '.png')
+
+ def show(self):
+ from PIL import Image
+ from io import BytesIO
+ f = BytesIO()
+ surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
+ self.width, self.height)
+ cr = cairo.Context(surface)
+ cr.set_source_surface(self.surface, 0, 0)
+ cr.paint()
+ surface.write_to_png(f)
+ f.seek(0)
+ im = Image.open(f)
+ im.show()
+
+ def _draw_polygon(self, verts):
+ cr = self.cr
+
+ v = verts[0]
+ cr.move_to(v[0], v[1])
+ for v in verts[1:]:
+ cr.line_to(v[0], v[1])
+ cr.close_path()
+
+ def _fill(self, fill_color, preserve=False):
+ if fill_color:
+ cr = self.cr
+ r, g, b, a = self.color_to_rgba(fill_color)
+ cr.set_source_rgba(r, g, b, a)
+ if preserve:
+ cr.fill_preserve()
+ else:
+ cr.fill()
+
+ def _stroke(self, stroke_color, preserve=False):
+ if stroke_color:
+ cr = self.cr
+ r, g, b, a = self.color_to_rgba(stroke_color)
+ cr.set_source_rgba(r, g, b, a)
+ if preserve:
+ cr.stroke_preserve()
+ else:
+ cr.stroke()
+
+ def draw_polygon_by_verts(self, verts,
+ stroke_color=(0, 0, 0),
+ fill_color=None):
+ cr = self.cr
+
+ cr.save()
+ self._draw_polygon(verts)
+ self._fill(fill_color, preserve=True)
+ self._stroke(stroke_color)
+ cr.restore()
+
+ def draw_star_by_verts(self, cx, cy, verts, stroke_color=(0, 0, 0)):
+ cr = self.cr
+
+ for v in verts:
+ cr.move_to(cx, cy)
+ cr.line_to(v[0], v[1])
+
+ self._stroke(stroke_color)
+
+ def draw_circle(self, cx, cy, radius=10.0,
+ stroke_color=None,
+ fill_color=(0, 0, 0, 0.5)):
+ cr = self.cr
+
+ cr.save()
+
+ cr.arc(cx, cy, radius, 0, 2 * pi)
+ self._fill(fill_color, preserve=True)
+ self._stroke(stroke_color)
+
+ cr.restore()
+
+ def draw_line(self, x1, y1, x2, y2, stroke_color=(0, 0, 0, 1)):
+ cr = self.cr
+ cr.move_to(x1, y1)
+ cr.line_to(x2, y2)
+ self._stroke(stroke_color)
+
+ def draw_rect_from_center(self, cx, cy, width, height, theta=0.0,
+ stroke_color=None,
+ fill_color=(1, 1, 1, 0.8)):
+ # the position of the center of a rectangle at (0,0)
+ mx = width / 2.0
+ my = height / 2.0
+
+ # calculate the position of the bottom-left corner after rotating the
+ # rectangle around the center
+ rx = cx - (mx * cos(theta) - my * sin(theta))
+ ry = cy - (mx * sin(theta) + my * cos(theta))
+
+ self.draw_rect(rx, ry, width, height, theta, stroke_color, fill_color)
+
+ def draw_rect(self, x, y, width, height, theta=0,
+ stroke_color=None,
+ fill_color=(1, 1, 1, 0.8)):
+ cr = self.cr
+
+ cr.save()
+ cr.translate(x, y)
+ cr.rotate(theta)
+
+ cr.rectangle(0, 0, width, height)
+ self._fill(fill_color, preserve=True)
+ self._stroke(stroke_color)
+
+ cr.restore()
+
+ def draw_centered_text(self, cx, cy, text, theta=0.0,
+ color=(0, 0, 0),
+ align_baseline=False,
+ bb_stroke_color=None,
+ bb_fill_color=None):
+ cr = self.cr
+
+ x_bearing, y_bearing, width, height, x_advance = cr.text_extents(text)[:5]
+ ascent, descent = cr.font_extents()[:2]
+
+ # The offset of the lower-left corner of the text.
+ tx = width / 2.0 + x_bearing
+
+ if align_baseline:
+ # When aligning to the baseline it is convenient the make the
+ # bounding box depend on the font vertical extent and not from the
+ # text content.
+ ty = 0
+ bb = [0, descent, width, -ascent]
+ else:
+ ty = height / 2.0 + y_bearing
+ bb = [0, y_bearing, width, height]
+
+ # The coordinate of the lower-left corner of the rotated rectangle
+ rx = cx - tx * cos(theta) + ty * sin(theta)
+ ry = cy - tx * sin(theta) - ty * cos(theta)
+
+ cr.save()
+ cr.translate(rx, ry)
+ cr.rotate(theta)
+
+ if bb_stroke_color or bb_fill_color:
+ self.draw_rect(bb[0], bb[1], bb[2], bb[3], 0,
+ bb_stroke_color,
+ bb_fill_color)
+
+ r, g, b, a = self.color_to_rgba(color)
+ cr.set_source_rgba(r, g, b, a)
+
+ cr.move_to(0, 0)
+ cr.show_text(text)
+
+ cr.restore()
+
+ return x_advance
+
+
+def test():
+ diagram = CairoDiagram(400, 400)
+
+ diagram.clear()
+
+ x = 40
+ y = 200
+
+ x_offset = x
+
+ theta = 0
+
+ diagram.draw_line(0, y, 400, y, (1, 0, 0, 1))
+
+ advance = diagram.draw_centered_text(x_offset, y, "Ciao", theta,
+ align_baseline=True,
+ bb_stroke_color=(0, 0, 0, 0.5),
+ bb_fill_color=(1, 1, 1, 0.8))
+ x_offset += advance
+
+ advance = diagram.draw_centered_text(x_offset, y, "____", theta + pi / 4,
+ align_baseline=True,
+ bb_stroke_color=(0, 0, 0, 0.5),
+ bb_fill_color=(1, 1, 1, 0.8))
+ x_offset += advance
+
+ advance = diagram.draw_centered_text(x_offset, y, "jxpqdlf", theta + pi / 2,
+ align_baseline=True,
+ bb_stroke_color=(0, 0, 0, 0.5),
+ bb_fill_color=(1, 1, 1, 0.8))
+ x_offset += advance
+
+ advance = diagram.draw_centered_text(x_offset, y, "pppp", theta + 3 * pi / 4,
+ align_baseline=True,
+ bb_stroke_color=(0, 0, 0, 0.5),
+ bb_fill_color=(1, 1, 1, 0.8))
+ x_offset += advance
+
+ advance = diagram.draw_centered_text(x_offset, y, "dddd", theta + pi,
+ align_baseline=True,
+ bb_stroke_color=(0, 0, 0, 0.5),
+ bb_fill_color=(1, 1, 1, 0.8))
+ x_offset += advance
+
+ advance = diagram.draw_centered_text(x_offset, y, "Jjjj", theta + 5 * pi / 4,
+ align_baseline=True,
+ bb_stroke_color=(0, 0, 0, 0.5),
+ bb_fill_color=(1, 1, 1, 0.8))
+ x_offset += advance
+
+ advance = diagram.draw_centered_text(x_offset, y, "1369", theta + 3 * pi / 2,
+ color=(0, 1, 0),
+ align_baseline=True,
+ bb_stroke_color=(0, 0, 0, 0.5),
+ bb_fill_color=(1, 1, 1, 0.8))
+ x_offset += advance
+
+ advance = diagram.draw_centered_text(x_offset, y, "qqqq", theta + 7 * pi / 4,
+ align_baseline=True,
+ bb_stroke_color=(0, 0, 0, 0.5),
+ bb_fill_color=(1, 1, 1, 0.8))
+ x_offset += advance
+
+ diagram.draw_rect(40, 40, 300, 100, stroke_color=(0, 0, 0, 0.8))
+ diagram.draw_rect(40, 40, 300, 100, pi / 30, stroke_color=(0, 0, 0, 0.8))
+
+ verts = diagram.draw_regular_polygon(190, 90, 3, 20)
+
+ diagram.draw_rect(40, 250, 300, 100, stroke_color=(0, 0, 0, 0.8))
+ diagram.draw_rect_from_center(40 + 150, 250 + 50, 300, 100, theta=(pi / 40),
+ stroke_color=(1, 0, 0),
+ fill_color=None)
+
+ verts = diagram.draw_regular_polygon(190, 300, 6, 20, pi / 3., (0, 0, 1, 0.5), (0, 1, 0.5))
+ diagram.draw_apothem_star(190, 300, 6, 20, 0, (1, 0, 1))
+
+ diagram.draw_star_by_verts(190, 300, verts, (1, 0, 0, 0.5))
+ diagram.draw_star(190, 300, 6, 25, 0, (1, 0, 1, 0.2))
+
+ diagram.draw_circle(190, 300, 30, (0, 1, 0, 0.5), None)
+ diagram.draw_circle(100, 300, 30, (1, 0, 0, 0.5), (0, 1, 1, 0.5))
+
+ diagram.show()
+
+
+if __name__ == "__main__":
+ test()
--- /dev/null
+#!/usr/bin/env python
+#
+# A Diagram base class
+#
+# Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from math import cos, sin, pi, fmod
+
+
+class Diagram(object):
+ def __init__(self, width, height, background=(1, 1, 1), font_size=20, stroke_width=2):
+ self.width = width
+ self.height = height
+ self.background = background
+ self.font_size = font_size
+ self.stroke_width = stroke_width
+
+ def clear(self):
+ raise NotImplementedError
+
+ @staticmethod
+ def color_to_rgba(color):
+ assert len(color) >= 3
+
+ color = tuple(float(c) for c in color)
+ if len(color) == 3:
+ color += (1.0,)
+
+ return color
+
+ @staticmethod
+ def normalized_angle_01(theta):
+ return fmod(theta, 2 * pi) / (2 * pi)
+
+ @staticmethod
+ def get_regular_polygon(x, y, sides, r, theta0=0.0):
+ """Calc the coordinates of the regular polygon.
+
+ NOTE: the first point will be in axis with y."""
+ theta = 2 * pi / sides
+
+ verts = []
+ for i in range(sides):
+ px = x + r * sin(theta0 + i * theta)
+ py = y + r * cos(theta0 + i * theta)
+ verts.append((px, py))
+
+ return verts
+
+ def draw_polygon_by_verts(self, verts,
+ stroke_color=(0, 0, 0),
+ fill_color=None):
+ raise NotImplementedError
+
+ def draw_regular_polygon(self, cx, cy, sides, r, theta=0.0,
+ stroke_color=(0, 0, 0),
+ fill_color=None):
+ verts = self.get_regular_polygon(cx, cy, sides, r, theta)
+ self.draw_polygon_by_verts(verts, stroke_color, fill_color)
+ return verts
+
+ def draw_star_by_verts(self, cx, cy, verts, stroke_color=(0, 0, 0)):
+ raise NotImplementedError
+
+ def draw_star(self, cx, cy, sides, r, theta=0.0, stroke_color=(0, 0, 0)):
+ verts = self.get_regular_polygon(cx, cy, sides, r, theta)
+ self.draw_star_by_verts(cx, cy, verts, stroke_color)
+ return verts
+
+ def draw_apothem_star(self, cx, cy, sides, r, theta=0.0, stroke_color=(0, 0, 0)):
+ """Draw a star but calculate the regular polygon apothem from the passed radius."""
+ apothem = r * cos(pi / sides)
+ apothem_angle = theta + pi / sides
+
+ return self.draw_star(cx, cy, sides, apothem, apothem_angle, stroke_color)
--- /dev/null
+#!/usr/bin/env python
+#
+# A diagram class to draw in Gimp
+#
+# Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import warnings
+from gimpfu import *
+
+from .diagram import Diagram
+
+
+class GimpDiagram(Diagram):
+ def __init__(self, width, height, image, layer, **kwargs):
+ super(GimpDiagram, self).__init__(width, height, **kwargs)
+
+ self.image = image
+ self.drawable = layer
+
+ def color_to_rgba(self, color):
+ color = super(GimpDiagram, self).color_to_rgba(color)
+ if color[3] != 1.0:
+ warnings.warn("Warning, trasparent colors are not supported", stacklevel=4)
+
+ return color
+
+ def clear(self):
+ pass
+
+ def _polygon_path(self, verts):
+ path = pdb.gimp_vectors_new(self.image, "")
+
+ v0 = verts[0]
+ strokeid = pdb.gimp_vectors_bezier_stroke_new_moveto(path,
+ v0[0], v0[1])
+ for v in verts[1:]:
+ pdb.gimp_vectors_bezier_stroke_lineto(path, strokeid, v[0], v[1])
+
+ pdb.gimp_vectors_stroke_close(path, strokeid)
+
+ return path
+
+ def _fill_path(self, path, fill_color):
+ if fill_color:
+ orig_foreground = pdb.gimp_context_get_foreground()
+ orig_selection = pdb.gimp_selection_save(self.image)
+
+ color = self.color_to_rgba(fill_color)
+ pdb.gimp_context_set_foreground(color)
+
+ pdb.gimp_image_select_item(self.image, CHANNEL_OP_REPLACE, path)
+
+ pdb.gimp_edit_fill(self.drawable, FOREGROUND_FILL)
+
+ pdb.gimp_selection_load(orig_selection)
+ pdb.gimp_image_remove_channel(self.image, orig_selection)
+ pdb.gimp_context_set_foreground(orig_foreground)
+
+ def _stroke_path(self, path, stroke_color):
+ if stroke_color:
+ orig_paint_method = pdb.gimp_context_get_paint_method()
+ orig_foreground = pdb.gimp_context_get_foreground()
+ orig_brush = pdb.gimp_context_get_brush()
+
+ pdb.gimp_context_set_paint_method('gimp-paintbrush')
+
+ color = self.color_to_rgba(stroke_color)
+ pdb.gimp_context_set_foreground(color)
+ pdb.gimp_context_set_brush("1. Pixel")
+ pdb.gimp_context_set_brush_size(self.stroke_width)
+
+ pdb.gimp_edit_stroke_vectors(self.drawable, path)
+
+ pdb.gimp_context_set_brush(orig_brush)
+ pdb.gimp_context_set_foreground(orig_foreground)
+ pdb.gimp_context_set_paint_method(orig_paint_method)
+
+ def draw_star_by_verts(self, cx, cy, verts, stroke_color=(0, 0, 0)):
+ path = pdb.gimp_vectors_new(self.image, "")
+
+ for v in verts:
+ strokeid = pdb.gimp_vectors_bezier_stroke_new_moveto(path, cx, cy)
+ pdb.gimp_vectors_bezier_stroke_lineto(path, strokeid, v[0], v[1])
+
+ pdb.gimp_image_insert_vectors(self.image, path, None, -1)
+ pdb.gimp_image_set_active_vectors(self.image, path)
+
+ self._stroke_path(path, stroke_color)
+
+ pdb.gimp_image_remove_vectors(self.image, path)
+
+ def draw_polygon_by_verts(self, verts,
+ stroke_color=(0, 0, 0),
+ fill_color=None):
+ path = self._polygon_path(verts)
+
+ pdb.gimp_image_insert_vectors(self.image, path, None, -1)
+ pdb.gimp_image_set_active_vectors(self.image, path)
+
+ self._fill_path(path, fill_color)
+ self._stroke_path(path, stroke_color)
+
+ pdb.gimp_image_remove_vectors(self.image, path)
+
+ def copy_polygon(self, src_drawable, verts, dest_drawable, matrix):
+ # flatten the verts list to be accepted by gimp_image_select_polygon()
+ segs = [coord for v in verts for coord in v]
+ pdb.gimp_image_select_polygon(self.image, CHANNEL_OP_REPLACE, len(segs), segs)
+
+ pdb.gimp_edit_copy(src_drawable)
+ floating_layer = pdb.gimp_edit_paste(dest_drawable, FALSE)
+
+ pdb.gimp_item_transform_matrix(floating_layer,
+ matrix[0], matrix[1], matrix[2],
+ matrix[3], matrix[4], matrix[5],
+ matrix[6], matrix[7], matrix[8])
+
+ pdb.gimp_floating_sel_anchor(floating_layer)
+
+ def draw_centered_text(self, cx, cy, text, theta, color,
+ align_baseline=False,
+ bb_stroke_color=None,
+ bb_fill_color=None):
+ font_name = "Georgia"
+
+ width, height, ascent, descent = pdb.gimp_text_get_extents_fontname(text,
+ self.font_size,
+ PIXELS,
+ font_name)
+
+ tx = cx - width / 2.0
+ ty = cy - height / 2.0
+
+ floating_selection = pdb.gimp_text_fontname(self.image, self.drawable,
+ tx, ty, text, 0, True,
+ self.font_size,
+ PIXELS,
+ font_name)
+
+ text_color = self.color_to_rgba(color)
+ pdb.gimp_text_layer_set_color(floating_selection, text_color)
+
+ pdb.gimp_item_transform_rotate(floating_selection, theta, FALSE, cx, cy)
+ pdb.gimp_floating_sel_anchor(floating_selection)
+
+ if align_baseline:
+ warnings.warn("The align_baseline option has not been implemented yet.")
+
+ if bb_stroke_color or bb_fill_color:
+ warnings.warn("Drawing the bounding box has not been implemented yet.")
--- /dev/null
+#!/usr/bin/env python
+#
+# A Diagram abstraction based on svgwrite
+#
+# Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import warnings
+from math import degrees
+import svgwrite
+from svgwrite.data.types import SVGAttribute
+from .diagram import Diagram
+
+
+class InkscapeDrawing(svgwrite.Drawing):
+ """An svgwrite.Drawing subclass which supports Inkscape layers"""
+ def __init__(self, *args, **kwargs):
+ super(InkscapeDrawing, self).__init__(*args, **kwargs)
+
+ inkscape_attributes = {
+ 'xmlns:inkscape': SVGAttribute('xmlns:inkscape',
+ anim=False,
+ types=[],
+ const=frozenset(['http://www.inkscape.org/namespaces/inkscape'])),
+ 'inkscape:groupmode': SVGAttribute('inkscape:groupmode',
+ anim=False,
+ types=[],
+ const=frozenset(['layer'])),
+ 'inkscape:label': SVGAttribute('inkscape:label',
+ anim=False,
+ types=frozenset(['string']),
+ const=[])
+ }
+
+ self.validator.attributes.update(inkscape_attributes)
+
+ elements = self.validator.elements
+
+ svg_attributes = set(elements['svg'].valid_attributes)
+ svg_attributes.add('xmlns:inkscape')
+ elements['svg'].valid_attributes = frozenset(svg_attributes)
+
+ g_attributes = set(elements['g'].valid_attributes)
+ g_attributes.add('inkscape:groupmode')
+ g_attributes.add('inkscape:label')
+ elements['g'].valid_attributes = frozenset(g_attributes)
+
+ self['xmlns:inkscape'] = 'http://www.inkscape.org/namespaces/inkscape'
+
+ def layer(self, **kwargs):
+ """Create an inkscape layer.
+
+ An optional 'label' keyword argument can be passed to set a user
+ friendly name for the layer."""
+ label = kwargs.pop('label', None)
+
+ new_layer = self.g(**kwargs)
+ new_layer['inkscape:groupmode'] = 'layer'
+
+ if label:
+ new_layer['inkscape:label'] = label
+
+ return new_layer
+
+
+class SvgwriteDiagram(Diagram):
+ def __init__(self, width, height, **kwargs):
+ super(SvgwriteDiagram, self).__init__(width, height, **kwargs)
+
+ self.svg = InkscapeDrawing(None, profile='full', size=(str(width) + "px", str(height) + "px"))
+ self.active_group = self.svg
+
+ def save_svg(self, filename):
+ self.svg.saveas(filename)
+
+ def add(self, element):
+ self.active_group.add(element)
+
+ def color_to_rgba(self, color):
+ color = super(SvgwriteDiagram, self).color_to_rgba(color)
+
+ return color[0] * 255, color[1] * 255, color[2] * 255, color[3]
+
+ def _fill(self, element, fill_color):
+ if fill_color:
+ r, g, b, a = self.color_to_rgba(fill_color)
+ fill_color = svgwrite.utils.rgb(r, g, b, mode='RGB')
+ element['fill'] = fill_color
+ element['fill-opacity'] = a
+ else:
+ element['fill'] = 'none'
+
+ def _stroke(self, element, stroke_color):
+ if stroke_color:
+ r, g, b, a = self.color_to_rgba(stroke_color)
+ stroke_color = svgwrite.utils.rgb(r, g, b, mode='RGB')
+ element['stroke'] = stroke_color
+ element['stroke-opacity'] = a
+ element['stroke-linejoin'] = 'round'
+ else:
+ element['stroke'] = 'none'
+
+ def draw_polygon_by_verts(self, verts,
+ stroke_color=(0, 0, 0),
+ fill_color=None):
+ polygon = self.svg.polygon(verts, stroke_width=self.stroke_width)
+
+ self._fill(polygon, fill_color)
+ self._stroke(polygon, stroke_color)
+
+ self.add(polygon)
+
+ def draw_star_by_verts(self, cx, cy, verts, stroke_color=(0, 0, 0)):
+ for v in verts:
+ line = self.svg.line((cx, cy), v, stroke_width=self.stroke_width)
+ self._stroke(line, stroke_color)
+ self.add(line)
+
+ def draw_centered_text(self, cx, cy, text, theta=0.0,
+ color=(0, 0, 0),
+ align_baseline=False,
+ bb_stroke_color=None,
+ bb_fill_color=None):
+
+ # Using font_size to calculate dy is not optimal as the font _height_ may
+ # be different from the font_size, but it's better than nothing.
+ text_element = self.svg.text(text, x=[cx], y=[cy], dy=[self.font_size / 2.])
+ self._fill(text_element, color)
+ text_element['font-size'] = self.font_size
+ text_element['text-anchor'] = 'middle'
+ text_element['transform'] = 'rotate(%f, %f, %f)' % (degrees(theta), cx, cy)
+ self.add(text_element)
+
+ if align_baseline:
+ warnings.warn("The align_baseline option has not been implemented yet.")
+
+ if bb_stroke_color or bb_fill_color:
+ warnings.warn("Drawing the bounding box has not been implemented yet.")
--- /dev/null
+#!/usr/bin/env python
+
+from .hexaflexagon_diagram import HexaflexagonDiagram
--- /dev/null
+#!/usr/bin/env python
+#
+# An class to draw hexaflexagons
+#
+# Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from math import sin, cos, pi
+from .trihexaflexagon import TriHexaflexagon
+
+
+class HexaflexagonDiagram(object):
+ def __init__(self, x_border, backend=None):
+ self.x_border = x_border
+ self.backend = backend
+
+ self.hexaflexagon = TriHexaflexagon()
+
+ num_hexagons = len(self.hexaflexagon.hexagons)
+ self.hexagon_radius = (self.backend.width - (x_border * (num_hexagons + 1))) / (num_hexagons * 2)
+
+ # The hexagon apothem is the triangle height
+ hexagon_apothem = self.hexagon_radius * cos(pi / 6.)
+
+ # the triangle radius is 2/3 of its height
+ self.triangle_radius = hexagon_apothem * 2. / 3.
+
+ self._init_centers()
+
+ # draw the plan centered wrt. the hexagons
+ self.plan_origin = (self.x_border * 2. + self.hexagon_radius / 2.,
+ self.x_border + self.triangle_radius / 3.)
+
+ self.hexagons_color_map = [(1, 0, 0), (0, 1, 0), (0, 0, 1)]
+
+ def _init_centers(self):
+ # Preallocate the lists to be able to access them by indices in the
+ # loops below.
+ self.hexagons_centers = [None for h in self.hexaflexagon.hexagons]
+ self.triangles_centers = [[None for t in h.triangles] for h in self.hexaflexagon.hexagons]
+
+ cy = self.backend.height - (self.hexagon_radius + self.x_border)
+ for hexagon in self.hexaflexagon.hexagons:
+ cx = self.x_border + self.hexagon_radius + (2 * self.hexagon_radius + self.x_border) * hexagon.index
+ self.hexagons_centers[hexagon.index] = (cx, cy)
+
+ triangles_centers = self.backend.get_regular_polygon(cx, cy, 6, self.triangle_radius)
+ for triangle in hexagon.triangles:
+ self.triangles_centers[hexagon.index][triangle.index] = triangles_centers[triangle.index]
+
+ def get_hexagon_center(self, hexagon):
+ return self.hexagons_centers[hexagon.index]
+
+ def get_triangle_center(self, triangle):
+ return self.triangles_centers[triangle.hexagon.index][triangle.index]
+
+ def get_triangle_center_in_plan(self, triangle):
+ x0, y0 = self.plan_origin
+ i, j = self.hexaflexagon.get_triangle_plan_position(triangle)
+ x, y = triangle.calc_plan_coordinates(self.triangle_radius, i, j)
+ return x0 + x, y0 + y
+
+ def get_triangle_verts(self, triangle):
+ cx, cy = self.get_triangle_center(triangle)
+ theta = triangle.get_angle_in_hexagon()
+ verts = self.backend.get_regular_polygon(cx, cy, 3, self.triangle_radius, theta)
+ return verts
+
+ def get_triangle_transform(self, triangle):
+ """Calculate the transformation matrix from a triangle in an hexagon to
+ the correspondent triangle in the plan.
+
+ Return the matrix as a list of values sorted in row-major order."""
+
+ src_x, src_y = self.get_triangle_center(triangle)
+ dest_x, dest_y = self.get_triangle_center_in_plan(triangle)
+ theta = triangle.get_angle_in_plan_relative_to_hexagon()
+
+ # The transformation from a triangle in the hexagon to the correspondent
+ # triangle in the plan is composed by these steps:
+ #
+ # 1. rotate by 'theta' around (src_x, src_y);
+ # 2. move to (dest_x, dest_y).
+ #
+ # Step 1 can be expressed by these sub-steps:
+ #
+ # 1a. translate by (-src_x, -src_y)
+ # 1b. rotate by 'theta'
+ # 1c. translate by (src_x, src_y)
+ #
+ # Step 2. can be expressed by a translation like:
+ #
+ # 2a. translate by (dest_x - src_x, dest_y - src_y)
+ #
+ # The consecutive translations 1c and 2a can be easily combined, so
+ # the final steps are:
+ #
+ # T1 -> translate by (-src_x, -src_y)
+ # R -> rotate by 'theta'
+ # T2 -> translate by (dest_x, dest_y)
+ #
+ # Using affine transformations these are expressed as:
+ #
+ # | 1 0 -src_x |
+ # T1 = | 0 1 -src_y |
+ # | 0 0 1 |
+ #
+ # | cos(theta) -sin(theta) 0 |
+ # R = | sin(theta) con(theta) 0 |
+ # | 0 0 1 |
+ #
+ # | 1 0 dest_x |
+ # T2 = | 0 1 dest_y |
+ # | 0 0 1 |
+ #
+ # Composing these transformations into one is achieved by multiplying
+ # the matrices from right to left:
+ #
+ # T = T2 * R * T1
+ #
+ # NOTE: To remember this think about composing functions: T2(R(T1())),
+ # the inner one is performed first.
+ #
+ # The resulting T matrix is the one below.
+ matrix = [
+ cos(theta), -sin(theta), -src_x * cos(theta) + src_y * sin(theta) + dest_x,
+ sin(theta), cos(theta), -src_x * sin(theta) - src_y * cos(theta) + dest_y,
+ 0, 0, 1
+ ]
+
+ return matrix
+
+ def draw_hexagon_template(self, hexagon):
+ for triangle in hexagon.triangles:
+ cx, cy = self.get_triangle_center(triangle)
+ theta = triangle.get_angle_in_hexagon()
+ self.draw_triangle_template(triangle, cx, cy, theta)
+
+ def draw_triangle_template(self, triangle, cx, cy, theta):
+ radius = self.triangle_radius
+ color = self.hexagons_color_map[triangle.hexagon.index]
+
+ tverts = self.backend.draw_regular_polygon(cx, cy, 3, radius, theta, color)
+
+ self.backend.draw_apothem_star(cx, cy, 3, radius, theta, color)
+
+ # Because of how draw_regular_polygon() is implemented, triangles are
+ # drawn by default with the base on the top, so the text need to be
+ # rotated by 180 to look like it is in the same orientation as
+ # a triangle with the base on the bottom.
+ text_theta = pi - theta
+
+ # Draw the text closer to the vertices of the element
+ t = 0.3
+
+ corners_labels = "ABC"
+ for i, v in enumerate(tverts):
+ tx = (1 - t) * v[0] + t * cx
+ ty = (1 - t) * v[1] + t * cy
+ corner_text = str(triangle.index + 1) + corners_labels[i]
+ self.backend.draw_centered_text(tx, ty, corner_text, text_theta, color)
+
+ def draw_plan_template(self):
+ x0, y0 = self.plan_origin
+ for hexagon in self.hexaflexagon.hexagons:
+ for triangle in hexagon.triangles:
+ i, j = self.hexaflexagon.get_triangle_plan_position(triangle)
+ x, y = triangle.calc_plan_coordinates(self.triangle_radius, i, j)
+ theta = triangle.get_angle_in_plan()
+ self.draw_triangle_template(triangle, x0 + x, y0 + y, theta)
+
+ def draw_template(self):
+ for hexagon in self.hexaflexagon.hexagons:
+ self.draw_hexagon_template(hexagon)
+
+ self.draw_plan_template()
--- /dev/null
+#!/usr/bin/env python
+#
+# A generic model for a tri-hexaflexagon
+#
+# Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from math import cos, sin, pi
+
+
+class Triangle(object):
+ def __init__(self, hexagon, index):
+ self.hexagon = hexagon
+ self.index = index
+
+ @staticmethod
+ def calc_plan_coordinates(radius, i, j):
+ apothem = radius * cos(pi / 3.)
+ side = 2. * radius * sin(pi / 3.)
+ width = side
+ height = apothem * 3.
+
+ xoffset = (j + 1) * width / 2.
+ yoffset = (i + (((i + j + 1) % 2) + 1) / 3.) * height
+
+ return xoffset, yoffset
+
+ def get_angle_in_plan(self):
+ """The angle of a triangle in the hexaflexagon plan."""
+ return - ((self.index + 1) % 2) * pi / 3.
+
+ def get_angle_in_plan_relative_to_hexagon(self):
+ """"Get the angle of the triangle in the plan relative to the rotation
+ of the same triangle in the hexagon."""
+ return ((self.index + 4) % 6 // 2) * pi * 2. / 3.
+
+ def get_angle_in_hexagon(self):
+ """Get the angle of the triangle in the hexagons.
+
+ NOTE: the angle is rotated by pi to have the first triangle with the
+ base on the bottom."""
+ return pi + self.index * pi / 3.
+
+ def __str__(self):
+ return "%d,%d" % (self.hexagon.index, self.index)
+
+
+class Hexagon(object):
+ def __init__(self, index):
+ self.index = index
+ self.triangles = []
+ for i in range(6):
+ triangle = Triangle(self, i)
+ self.triangles.append(triangle)
+
+ def __str__(self):
+ output = ""
+ for i in range(0, 6):
+ output += str(self.triangles[i])
+ output += "\t"
+
+ return output
+
+
+class TriHexaflexagon(object):
+ def __init__(self):
+ self.hexagons = []
+ for i in range(0, 3):
+ hexagon = Hexagon(i)
+ self.hexagons.append(hexagon)
+
+ # A plan is described by a mapping of the triangles in the hexagons,
+ # repositioned on a 2d grid.
+ #
+ # In the map below, the grid has two rows, each element of the grid is
+ # a pair (h, t), where 'h' is the index of the hexagon, and 't' is the
+ # index of the triangle in that hexagon.
+ plan_map = [
+ [(0, 0), (1, 5), (1, 4), (2, 3), (2, 2), (0, 3), (0, 2), (1, 1), (1, 0)],
+ [(2, 5), (2, 4), (0, 5), (0, 4), (1, 3), (1, 2), (2, 1), (2, 0), (0, 1)]
+ ]
+
+ # Preallocate a bi-dimensional array for an inverse mapping, this is
+ # useful to retrieve the position in the plan given a triangle.
+ self.plan_map_inv = [[-1 for t in h.triangles] for h in self.hexagons]
+
+ self.plan = []
+ for i, plan_map_row in enumerate(plan_map):
+ plan_row = []
+ for j, mapping in enumerate(plan_map_row):
+ hexagon_index, triangle_index = mapping
+ hexagon = self.hexagons[hexagon_index]
+ triangle = hexagon.triangles[triangle_index]
+ plan_row.append(triangle)
+
+ self.plan_map_inv[hexagon_index][triangle_index] = (i, j)
+
+ self.plan.append(plan_row)
+
+ def get_triangle_plan_position(self, triangle):
+ return self.plan_map_inv[triangle.hexagon.index][triangle.index]
+
+ def __str__(self):
+ output = ""
+
+ for row in self.plan:
+ for triangle in row:
+ output += "%s\t" % str(triangle)
+ output += "\n"
+
+ return output
+
+
+def test():
+ trihexaflexagon = TriHexaflexagon()
+ print(trihexaflexagon)
+
+
+if __name__ == "__main__":
+ test()
--- /dev/null
+#!/usr/bin/env python
+
+'''
+Gimp plugin "TestGimpDiagram"
+
+Test the GImpDiagram class
+
+Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+from math import pi
+import gimpfu
+from gimpfu import *
+
+from diagram.gimp_diagram import GimpDiagram
+
+gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
+
+
+def test_diagram_main(image):
+ pdb.gimp_image_undo_group_start(image)
+ pdb.gimp_context_push()
+ pdb.gimp_context_set_defaults()
+
+ template_layer_name = "TestGimpDiagram"
+ template_layer = pdb.gimp_image_get_layer_by_name(image,
+ template_layer_name)
+ if not template_layer:
+ template_layer = pdb.gimp_layer_new(image, image.width, image.height,
+ gimpfu.RGBA_IMAGE,
+ template_layer_name,
+ 100,
+ gimpfu.NORMAL_MODE)
+ pdb.gimp_image_add_layer(image, template_layer, -1)
+
+ diagram = GimpDiagram(image.width, image.height, image, template_layer,
+ font_size=10, stroke_size=2)
+
+ diagram.draw_regular_polygon(200, 200, 6, 100, 0,
+ fill_color=(1, 1, 0))
+
+ diagram.draw_regular_polygon(200, 200, 6, 100, pi / 12.,
+ stroke_color=(1, 0, 0, 0.2))
+
+ diagram.draw_centered_text(200, 200, "__30__", pi / 6., (0, 0, 0))
+
+ pdb.gimp_context_pop()
+ pdb.gimp_image_undo_group_end(image)
+
+
+if __name__ == "__main__":
+ register(
+ "python_fu_test_gimp_diagram",
+ N_("Test GimpDiagram"),
+ "Test GimpDiagram",
+ "Antonio Ospite <ao2@ao2.it>",
+ "Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>",
+ "2017",
+ N_("TestGImpDiagram..."),
+ "RGB*, GRAY*",
+ [
+ (PF_IMAGE, "image", "Input image", None),
+ ],
+ [],
+ test_diagram_main,
+ menu="<Image>/Filters/Render",
+ domain=("gimp20-python", gimp.locale_directory)
+ )
+
+ main()
--- /dev/null
+#!/usr/bin/env python
+
+'''
+Gimp plugin "Hexaflexagon"
+
+Create Hexaflexagons.
+
+Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+# The plugin is inspired to flexagon.scm by Andrea Rossetti:
+# http://andrear.altervista.org/home/gimp_flexagon.php
+#
+# It has been rewritten in python in the hope to simplify it and attract more
+# contributors.
+
+from gimpfu import *
+
+from diagram.gimp_diagram import GimpDiagram
+from flexagon import HexaflexagonDiagram
+
+gettext.install("gimp20-python", gimp.locale_directory, unicode=True)
+
+
+def build_plan(template_layer, hexaflexagon_layer, diagram):
+ for hexagon in diagram.hexaflexagon.hexagons:
+ for triangle in hexagon.triangles:
+ verts = diagram.get_triangle_verts(triangle)
+
+ matrix = diagram.get_triangle_transform(triangle)
+ diagram.backend.copy_polygon(template_layer, verts, hexaflexagon_layer, matrix)
+
+
+def hexaflexagon_main(image):
+ x_border = image.width / 50
+ font_size = image.width / 80
+ stroke_width = image.width / 480
+
+ template_layer_name = "HexaflexagonTemplate"
+ content_layer_name = "Hexagons"
+ hexaflexagon_layer_name = "Hexaflexagon"
+
+ message = "Draw the hexagons content into the '%s' layer.\n" % content_layer_name
+ message += "Then call this script again."
+
+ pdb.gimp_image_undo_group_start(image)
+ pdb.gimp_context_push()
+ pdb.gimp_context_set_defaults()
+
+ template_layer = pdb.gimp_image_get_layer_by_name(image,
+ template_layer_name)
+
+ content_layer = pdb.gimp_image_get_layer_by_name(image,
+ content_layer_name)
+ if not content_layer:
+ content_layer = pdb.gimp_layer_new(image, image.width, image.height,
+ RGBA_IMAGE, content_layer_name,
+ 100, NORMAL_MODE)
+ if template_layer:
+ template_layer_position = pdb.gimp_image_get_item_position(image, template_layer)
+ content_layer_position = template_layer_position - 1
+ pdb.gimp_image_insert_layer(image, content_layer, None, content_layer_position)
+
+ pdb.gimp_message(message)
+
+ pdb.gimp_context_pop()
+ pdb.gimp_image_undo_group_end(image)
+ return
+ else:
+ content_layer_position = -1
+ pdb.gimp_image_insert_layer(image, content_layer, None, content_layer_position)
+
+ if not template_layer:
+ template_layer = pdb.gimp_layer_new(image, image.width, image.height,
+ RGBA_IMAGE, template_layer_name,
+ 100, NORMAL_MODE)
+ pdb.gimp_image_insert_layer(image, template_layer, None, -1)
+
+ gimp_backend = GimpDiagram(image.width, image.height,
+ image, template_layer,
+ font_size=font_size, stroke_width=stroke_width)
+ diagram = HexaflexagonDiagram(x_border, backend=gimp_backend)
+ diagram.draw_template()
+
+ pdb.gimp_message(message)
+ pdb.gimp_context_pop()
+ pdb.gimp_image_undo_group_end(image)
+ return
+
+ hexaflexagon_layer = pdb.gimp_image_get_layer_by_name(image,
+ hexaflexagon_layer_name)
+ if hexaflexagon_layer:
+ pdb.gimp_message("There is already a generated hexaflexagon.")
+ pdb.gimp_context_pop()
+ pdb.gimp_image_undo_group_end(image)
+ return
+
+ hexaflexagon_layer = pdb.gimp_layer_new(image, image.width,
+ image.height, RGBA_IMAGE,
+ hexaflexagon_layer_name, 100,
+ NORMAL_MODE)
+ pdb.gimp_image_insert_layer(image, hexaflexagon_layer, None, -1)
+
+ gimp_backend = GimpDiagram(image.width, image.height,
+ image, content_layer,
+ font_size=font_size, stroke_width=stroke_width)
+ diagram = HexaflexagonDiagram(x_border, backend=gimp_backend)
+ build_plan(content_layer, hexaflexagon_layer, diagram)
+
+ pdb.gimp_context_pop()
+ pdb.gimp_image_undo_group_end(image)
+
+
+if __name__ == "__main__":
+ register(
+ "python_fu_hexaflexagon",
+ N_("Create Hexaflexagons"),
+ "Create Hexaflexagons",
+ "Antonio Ospite <ao2@ao2.it>",
+ "Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>",
+ "2015",
+ N_("Hexaflexagon..."),
+ "RGB*, GRAY*",
+ [
+ (PF_IMAGE, "image", "Input image", None),
+ ],
+ [],
+ hexaflexagon_main,
+ menu="<Image>/Filters/Render",
+ domain=("gimp20-python", gimp.locale_directory)
+ )
+
+ main()
--- /dev/null
+#!/usr/bin/env python3
+#
+# Draw an SVG hexaflexagon which can be edited live in Inkscape.
+#
+# Copyright (C) 2018 Antonio Ospite <ao2@ao2.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from math import pi
+import svgwrite
+
+from diagram.svgwrite_diagram import SvgwriteDiagram
+from flexagon.hexaflexagon_diagram import HexaflexagonDiagram
+
+
+class SvgwriteHexaflexagonDiagram(HexaflexagonDiagram):
+ def __init__(self, *args, **kwargs):
+ super(SvgwriteHexaflexagonDiagram, self).__init__(*args, **kwargs)
+
+ svg = self.backend.svg
+
+ # create some layers and groups
+ layers = {
+ "Hexagons": svg.layer(label="Hexagons"),
+ "Hexaflexagon": svg.layer(label="Hexaflexagon"),
+ "Folding guide": svg.layer(label="Folding guide"),
+ "Template": svg.layer(label="Template")
+ }
+ for layer in layers.values():
+ svg.add(layer)
+
+ self.groups = layers
+
+ for hexagon in self.hexaflexagon.hexagons:
+ name = "hexagon%d-content" % hexagon.index
+ layer = svg.layer(id=name, label="Hexagon %d" % (hexagon.index + 1))
+ self.groups[name] = layer
+ layers['Hexagons'].add(layer)
+
+ for triangle in hexagon.triangles:
+ name = "hexagon%d-triangle%d" % (hexagon.index, triangle.index)
+ group = svg.g(id=name)
+ self.groups[name] = group
+ layers['Template'].add(group)
+
+ def draw(self):
+ for hexagon in self.hexaflexagon.hexagons:
+ cx, cy = self.get_hexagon_center(hexagon)
+
+ # Draw some default content
+ old_active_group = self.backend.active_group
+ self.backend.active_group = self.groups["hexagon%d-content" % hexagon.index]
+ self.backend.draw_regular_polygon(cx, cy, 6, self.hexagon_radius, pi / 6., fill_color=(0.5, 0.5, 0.5, 0.2), stroke_color=None)
+ self.backend.active_group = old_active_group
+
+ # Add folding guides
+ old_active_group = self.backend.active_group
+ self.backend.active_group = self.groups["Folding guide"]
+
+ for triangle in hexagon.triangles:
+ cx, cy = self.get_triangle_center(triangle)
+ theta = triangle.get_angle_in_hexagon()
+ self.backend.draw_regular_polygon(cx, cy, 3, self.triangle_radius, theta, (0, 0, 0, 0.2))
+ polygon = self.backend.active_group.elements[-1]
+ polygon['id'] = "hexagon%d-triangle%d-folding" % (triangle.hexagon.index, triangle.index)
+
+ self.backend.active_group = old_active_group
+
+ # Draw the normal template for hexagons
+ for hexagon in self.hexaflexagon.hexagons:
+ self.draw_hexagon_template(hexagon)
+
+ # draw plan using references
+ for hexagon in self.hexaflexagon.hexagons:
+ for triangle in hexagon.triangles:
+ m = self.get_triangle_transform(triangle)
+ svg_matrix = "matrix(%f, %f, %f, %f, %f, %f)" % (m[0], m[3],
+ m[1], m[4],
+ m[2], m[5])
+
+ # Reuse the hexagons triangle for the hexaflexagon template
+ group = self.groups["Template"]
+ triangle_href = "#hexagon%d-triangle%d" % (hexagon.index, triangle.index)
+ ref = self.backend.svg.use(triangle_href)
+ ref['transform'] = svg_matrix
+ group.add(ref)
+
+ # Reuse the folding guides
+ group = self.groups["Folding guide"]
+ folding_href = "#hexagon%d-triangle%d-folding" % (hexagon.index, triangle.index)
+ ref = self.backend.svg.use(folding_href)
+ ref['transform'] = svg_matrix
+ group.add(ref)
+
+ # Reuse the content to draw the final hexaflexagon
+ group = self.groups["Hexaflexagon"]
+ content_href = "#hexagon%d-content" % hexagon.index
+ ref = self.backend.svg.use(content_href)
+ ref['transform'] = svg_matrix
+ ref['clip-path'] = "url(%s)" % (triangle_href + '-clip-path')
+ group.add(ref)
+
+ def draw_triangle_template(self, triangle, cx, cy, theta):
+ old_active_group = self.backend.active_group
+ group_name = "hexagon%d-triangle%d" % (triangle.hexagon.index, triangle.index)
+ self.backend.active_group = self.groups[group_name]
+
+ super(SvgwriteHexaflexagonDiagram, self).draw_triangle_template(triangle, cx, cy, theta)
+
+ # The triangle outline in the active group's element is the only polygon
+ # element, so get it and set its id so that it can be reused as
+ # a clip-path
+ for element in self.backend.active_group.elements:
+ if isinstance(element, svgwrite.shapes.Polygon):
+ element['id'] = group_name + "-outline"
+ break
+
+ clip_path = self.backend.svg.clipPath(id=group_name + '-clip-path')
+ self.backend.svg.defs.add(clip_path)
+ ref = self.backend.svg.use('#%s-outline' % group_name)
+ clip_path.add(ref)
+
+ self.backend.active_group = old_active_group
+
+
+def main():
+ width = 3508
+ height = 2480
+
+ x_border = width / 50
+ font_size = width / 80
+ stroke_width = width / 480
+
+ svg_backend = SvgwriteDiagram(width, height, font_size=font_size, stroke_width=stroke_width)
+ hexaflexagon = SvgwriteHexaflexagonDiagram(x_border, backend=svg_backend)
+ hexaflexagon.draw()
+ svg_backend.save_svg("inkscape-hexaflexagon-editor.svg")
+
+
+if __name__ == "__main__":
+ main()