From d7db3a7d7112d2bf77c96d0f027bf0e00ab15215 Mon Sep 17 00:00:00 2001 From: Antonio Ospite Date: Wed, 27 Jun 2018 11:31:05 +0200 Subject: [PATCH] Add support for tetraflexagons --- src/flexagon/tetraflexagon_diagram.py | 168 ++++++++++++++++++++++++++++++++++ src/flexagon/tritetraflexagon.py | 121 ++++++++++++++++++++++++ src/svg_tetraflexagon_editor.py | 132 ++++++++++++++++++++++++++ 3 files changed, 421 insertions(+) create mode 100755 src/flexagon/tetraflexagon_diagram.py create mode 100755 src/flexagon/tritetraflexagon.py create mode 100755 src/svg_tetraflexagon_editor.py diff --git a/src/flexagon/tetraflexagon_diagram.py b/src/flexagon/tetraflexagon_diagram.py new file mode 100755 index 0000000..eb2c807 --- /dev/null +++ b/src/flexagon/tetraflexagon_diagram.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# +# An class to draw tetraflexagons +# +# Copyright (C) 2018 Antonio Ospite +# +# 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 . + +from math import sin, cos +from .tritetraflexagon import TriTetraflexagon + + +class TetraflexagonDiagram(object): + def __init__(self, x_border, backend=None): + self.x_border = x_border + self.backend = backend + + self.tetraflexagon = TriTetraflexagon() + + num_squares = len(self.tetraflexagon.squares) + self.square_side = (self.backend.height - (x_border * 3)) / (num_squares) + self.square_radius = self.square_side / 2 + self.tile_side = self.square_radius + self.tile_radius = self.tile_side / 2 + + self._init_centers() + + # draw the plan centered wrt. the squares + self.plan_origin = ((self.backend.width - self.tile_side * 5) / 2, + self.x_border) + + self.squares_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.squares_centers = [None for h in self.tetraflexagon.squares] + self.tiles_centers = [[None for t in h.tiles] for h in self.tetraflexagon.squares] + + cy = self.backend.height - (self.square_radius + self.x_border) + for square in self.tetraflexagon.squares: + cx = self.x_border + (2 * self.square_radius + self.x_border) * (square.index + 1) + self.squares_centers[square.index] = (cx, cy) + + for tile in square.tiles: + # offset by 1 or -1 times the tile radius + tile_cx = cx + self.tile_radius * ((tile.index % 2) * 2 - 1) + tile_cy = cy + self.tile_radius * ((tile.index > 1) * 2 - 1) + self.tiles_centers[square.index][tile.index] = (tile_cx, tile_cy) + + def get_square_center(self, square): + return self.squares_centers[square.index] + + def get_tile_center(self, tile): + return self.tiles_centers[tile.square.index][tile.index] + + def get_tile_center_in_plan(self, tile): + x0, y0 = self.plan_origin + i, j = self.tetraflexagon.get_tile_plan_position(tile) + x, y = tile.calc_plan_coordinates(self.tile_side, i, j) + return x0 + x, y0 + y + + def get_tile_transform(self, tile): + """Calculate the transformation matrix from a tile in an square to + the correspondent tile in the plan. + + Return the matrix as a list of values sorted in row-major order.""" + + src_x, src_y = self.get_tile_center(tile) + dest_x, dest_y = self.get_tile_center_in_plan(tile) + + i, j = self.tetraflexagon.get_tile_plan_position(tile) + theta = tile.get_angle_in_plan(i, j) + + # The transformation from a tile in the square to the correspondent + # tile 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_square_template(self, square): + for tile in square.tiles: + cx, cy = self.get_tile_center(tile) + self.draw_tile_template(tile, cx, cy, 0) + + def draw_tile_template(self, tile, cx, cy, theta): + side = self.tile_side + color = self.squares_color_map[tile.square.index] + + self.backend.draw_rect_from_center(cx, cy, side, side, theta, color) + + corners_labels = "ABC" + corner_text = corners_labels[tile.square.index] + str(tile.index + 1) + self.backend.draw_centered_text(cx, cy, corner_text, 0, color) + + def draw_plan_template(self): + x0, y0 = self.plan_origin + for square in self.tetraflexagon.squares: + for tile in square.tiles: + i, j = self.tetraflexagon.get_tile_plan_position(tile) + x, y = tile.calc_plan_coordinates(self.tile_radius, i, j) + theta = tile.get_angle_in_plan(i, j) + self.draw_tile_template(tile, x0 + x, y0 + y, theta) + + def draw_template(self): + for square in self.tetraflexagon.squares: + self.draw_square_template(square) + + self.draw_plan_template() diff --git a/src/flexagon/tritetraflexagon.py b/src/flexagon/tritetraflexagon.py new file mode 100755 index 0000000..8a70069 --- /dev/null +++ b/src/flexagon/tritetraflexagon.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +# +# A generic model for a tri-tetraflexagon +# +# Copyright (C) 2018 Antonio Ospite +# +# 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 . + +from math import pi + + +class Tile(object): + def __init__(self, square, index): + self.square = square + self.index = index + + @staticmethod + def calc_plan_coordinates(side, i, j): + xoffset = side / 2 + j * side + yoffset = side / 2 + i * side + + return xoffset, yoffset + + @staticmethod + def calc_angle_in_plan(i, j): + """The angle of a tile in the tetraflexagon plan.""" + return pi * (i > 1) + + def __str__(self): + return "%d,%d" % (self.square.index, self.index) + + +class Square(object): + def __init__(self, index): + self.index = index + self.tiles = [] + for i in range(4): + tile = Tile(self, i) + self.tiles.append(tile) + + def __str__(self): + output = "" + for i in range(0, 4): + output += str(self.tiles[i]) + output += "\t" + + return output + + +class TriTetraflexagon(object): + def __init__(self): + self.squares = [] + for i in range(0, 3): + square = Square(i) + self.squares.append(square) + + # A plan is described by a mapping of the tiles in the squares, + # repositioned on a 2d grid. + # + # In the map below, the grid has two rows, each element of the grid is + # a pair (s, t), where 's' is the index of the square, and 't' is the + # index of the tile in that square. + plan_map = [ + [(2, 0), (2, 1), (0, 1), None, None], + [None, None, (0, 3), (1, 2), (1, 3)], + [None, None, (2, 3), (2, 2), (0, 2)], + [(0, 0), (1, 1), (1, 0), None, None], + ] + + # Preallocate a bi-dimensional array for an inverse mapping, this is + # useful to retrieve the position in the plan given a tile. + self.plan_map_inv = [[-1 for t in h.tiles] for h in self.squares] + + self.plan = [] + for i, plan_map_row in enumerate(plan_map): + plan_row = [] + for j, mapping in enumerate(plan_map_row): + if mapping: + square_index, tile_index = mapping + square = self.squares[square_index] + tile = square.tiles[tile_index] + self.plan_map_inv[square_index][tile_index] = (i, j) + else: + tile = None + + plan_row.append(tile) + + self.plan.append(plan_row) + + def get_tile_plan_position(self, tile): + return self.plan_map_inv[tile.square.index][tile.index] + + def __str__(self): + output = "" + + for row in self.plan: + for tile in row: + output += "%s\t" % str(tile) + output += "\n" + + return output + + +def test(): + tritetraflexagon = TriTetraflexagon() + print(tritetraflexagon) + + +if __name__ == "__main__": + test() diff --git a/src/svg_tetraflexagon_editor.py b/src/svg_tetraflexagon_editor.py new file mode 100755 index 0000000..ce2794b --- /dev/null +++ b/src/svg_tetraflexagon_editor.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# +# Draw an SVG tetraflexagon which can be edited live in Inkscape. +# +# Copyright (C) 2018 Antonio Ospite +# +# 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 . + +import svgwrite + +from diagram.svgwrite_diagram import SvgwriteDiagram +from flexagon.tetraflexagon_diagram import TetraflexagonDiagram + + +class SvgwriteTetraflexagonDiagram(TetraflexagonDiagram): + def __init__(self, *args, **kwargs): + super(SvgwriteTetraflexagonDiagram, self).__init__(*args, **kwargs) + + svg = self.backend.svg + + # create some layers and groups + layers = { + "Squares": svg.layer(label="Squares"), + "Tetraflexagon": svg.layer(label="Tetraflexagon"), + "Template": svg.layer(label="Template") + } + for layer in layers.values(): + svg.add(layer) + + self.groups = layers + + for square in self.tetraflexagon.squares: + name = "square%d-content" % square.index + layer = svg.layer(id=name, label="Square %d" % (square.index + 1)) + self.groups[name] = layer + layers['Squares'].add(layer) + + for tile in square.tiles: + name = "square%d-tile%d" % (square.index, tile.index) + group = svg.g(id=name) + self.groups[name] = group + layers['Template'].add(group) + + def draw(self): + for square in self.tetraflexagon.squares: + cx, cy = self.get_square_center(square) + + # Draw some default content + old_active_group = self.backend.active_group + self.backend.active_group = self.groups["square%d-content" % square.index] + self.backend.draw_rect_from_center(cx, cy, self.square_side, self.square_side, 0, + fill_color=(0.5, 0.5, 0.5, 0.2)) + self.backend.active_group = old_active_group + + self.backend.active_group = old_active_group + + # Draw the normal template for squares + for square in self.tetraflexagon.squares: + self.draw_square_template(square) + + # draw plan using references + for square in self.tetraflexagon.squares: + for tile in square.tiles: + m = self.get_tile_transform(tile) + svg_matrix = "matrix(%f, %f, %f, %f, %f, %f)" % (m[0], m[3], + m[1], m[4], + m[2], m[5]) + + # Reuse the squares tile for the tetraflexagon template + group = self.groups["Template"] + tile_href = "#square%d-tile%d" % (square.index, tile.index) + ref = self.backend.svg.use(tile_href) + ref['transform'] = svg_matrix + group.add(ref) + + # Reuse the content to draw the final tetraflexagon + group = self.groups["Tetraflexagon"] + content_href = "#square%d-content" % square.index + ref = self.backend.svg.use(content_href) + ref['transform'] = svg_matrix + ref['clip-path'] = "url(%s)" % (tile_href + '-clip-path') + group.add(ref) + + def draw_tile_template(self, tile, cx, cy, theta): + old_active_group = self.backend.active_group + group_name = "square%d-tile%d" % (tile.square.index, tile.index) + self.backend.active_group = self.groups[group_name] + + super(SvgwriteTetraflexagonDiagram, self).draw_tile_template(tile, cx, cy, theta) + + # The tile 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.Rect): + 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) + tetraflexagon = SvgwriteTetraflexagonDiagram(x_border, backend=svg_backend) + tetraflexagon.draw() + svg_backend.save_svg("inkscape-tetraflexagon-editor.svg") + + +if __name__ == "__main__": + main() -- 2.1.4