3 # An class to draw hexaflexagons
 
   5 # Copyright (C) 2018  Antonio Ospite <ao2@ao2.it>
 
   7 # This program is free software: you can redistribute it and/or modify
 
   8 # it under the terms of the GNU General Public License as published by
 
   9 # the Free Software Foundation, either version 3 of the License, or
 
  10 # (at your option) any later version.
 
  12 # This program is distributed in the hope that it will be useful,
 
  13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 
  14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
  15 # GNU General Public License for more details.
 
  17 # You should have received a copy of the GNU General Public License
 
  18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
  20 from math import sin, cos, pi
 
  21 from .trihexaflexagon import TriHexaflexagon
 
  24 class HexaflexagonDiagram(object):
 
  25     def __init__(self, x_border, backend=None):
 
  26         self.x_border = x_border
 
  27         self.backend = backend
 
  29         self.hexaflexagon = TriHexaflexagon()
 
  31         num_hexagons = len(self.hexaflexagon.hexagons)
 
  32         self.hexagon_radius = (self.backend.width - (x_border * (num_hexagons + 1))) / (num_hexagons * 2)
 
  34         # The hexagon apothem is the triangle height
 
  35         hexagon_apothem = self.hexagon_radius * cos(pi / 6.)
 
  37         # the triangle radius is 2/3 of its height
 
  38         self.triangle_radius = hexagon_apothem * 2. / 3.
 
  42         # draw the plan centered wrt. the hexagons
 
  43         self.plan_origin = (self.x_border * 2. + self.hexagon_radius / 2.,
 
  44                             self.x_border + self.triangle_radius / 3.)
 
  46         self.hexagons_color_map = [(1, 0, 0), (0, 1, 0), (0, 0, 1)]
 
  48     def _init_centers(self):
 
  49         # Preallocate the lists to be able to access them by indices in the
 
  51         self.hexagons_centers = [None for h in self.hexaflexagon.hexagons]
 
  52         self.triangles_centers = [[None for t in h.triangles] for h in self.hexaflexagon.hexagons]
 
  54         cy = self.backend.height - (self.hexagon_radius + self.x_border)
 
  55         for hexagon in self.hexaflexagon.hexagons:
 
  56             cx = self.x_border + self.hexagon_radius + (2 * self.hexagon_radius + self.x_border) * hexagon.index
 
  57             self.hexagons_centers[hexagon.index] = (cx, cy)
 
  59             triangles_centers = self.backend.get_regular_polygon(cx, cy, 6, self.triangle_radius, pi / 6)
 
  60             for triangle in hexagon.triangles:
 
  61                 self.triangles_centers[hexagon.index][triangle.index] = triangles_centers[triangle.index]
 
  63     def get_hexagon_center(self, hexagon):
 
  64         return self.hexagons_centers[hexagon.index]
 
  66     def get_triangle_center(self, triangle):
 
  67         return self.triangles_centers[triangle.hexagon.index][triangle.index]
 
  69     def get_triangle_center_in_plan(self, triangle):
 
  70         x0, y0 = self.plan_origin
 
  71         i, j = self.hexaflexagon.get_triangle_plan_position(triangle)
 
  72         x, y = triangle.calc_plan_coordinates(self.triangle_radius, i, j)
 
  75     def get_triangle_verts(self, triangle):
 
  76         cx, cy = self.get_triangle_center(triangle)
 
  77         theta = triangle.get_angle_in_hexagon()
 
  78         verts = self.backend.get_regular_polygon(cx, cy, 3, self.triangle_radius, theta)
 
  81     def get_triangle_transform(self, triangle):
 
  82         """Calculate the transformation matrix from a triangle in an hexagon to
 
  83         the correspondent triangle in the plan.
 
  85         Return the matrix as a list of values sorted in row-major order."""
 
  87         src_x, src_y = self.get_triangle_center(triangle)
 
  88         dest_x, dest_y = self.get_triangle_center_in_plan(triangle)
 
  89         theta = triangle.get_angle_in_plan_relative_to_hexagon()
 
  91         # The transformation from a triangle in the hexagon to the correspondent
 
  92         # triangle in the plan is composed by these steps:
 
  94         #   1. rotate by 'theta' around (src_x, src_y);
 
  95         #   2. move to (dest_x, dest_y).
 
  97         # Step 1 can be expressed by these sub-steps:
 
  99         #  1a. translate by (-src_x, -src_y)
 
 100         #  1b. rotate by 'theta'
 
 101         #  1c. translate by (src_x, src_y)
 
 103         # Step 2. can be expressed by a translation like:
 
 105         #  2a. translate by (dest_x - src_x, dest_y - src_y)
 
 107         # The consecutive translations 1c and 2a can be easily combined, so
 
 108         # the final steps are:
 
 110         #  T1 -> translate by (-src_x, -src_y)
 
 111         #  R  -> rotate by 'theta'
 
 112         #  T2 -> translate by (dest_x, dest_y)
 
 114         # Using affine transformations these are expressed as:
 
 117         # T1 = | 0  1  -src_y |
 
 120         #      | cos(theta)  -sin(theta)  0 |
 
 121         # R  = | sin(theta)   con(theta)  0 |
 
 125         # T2 = | 0  1  dest_y |
 
 128         # Composing these transformations into one is achieved by multiplying
 
 129         # the matrices from right to left:
 
 133         # NOTE: To remember this think about composing functions: T2(R(T1())),
 
 134         # the inner one is performed first.
 
 136         # The resulting  T matrix is the one below.
 
 138             cos(theta), -sin(theta), -src_x * cos(theta) + src_y * sin(theta) + dest_x,
 
 139             sin(theta),  cos(theta), -src_x * sin(theta) - src_y * cos(theta) + dest_y,
 
 145     def draw_hexagon_template(self, hexagon):
 
 146         for triangle in hexagon.triangles:
 
 147             cx, cy = self.get_triangle_center(triangle)
 
 148             theta = triangle.get_angle_in_hexagon()
 
 149             self.draw_triangle_template(triangle, cx, cy, theta)
 
 151     def draw_triangle_template(self, triangle, cx, cy, theta):
 
 152         radius = self.triangle_radius
 
 153         color = self.hexagons_color_map[triangle.hexagon.index]
 
 155         tverts = self.backend.draw_regular_polygon(cx, cy, 3, radius, theta, color)
 
 157         self.backend.draw_apothem_star(cx, cy, 3, radius, theta, color)
 
 159         # Because of how draw_regular_polygon() is implemented, triangles are
 
 160         # drawn by default with the base on the top, so the text need to be
 
 161         # rotated by 180 to look like it is in the same orientation as
 
 162         # a triangle with the base on the bottom.
 
 163         text_theta = pi - theta
 
 165         # Draw the text closer to the vertices of the element
 
 168         corners_labels = "ABC"
 
 169         for i, v in enumerate(tverts):
 
 170             tx = (1 - t) * v[0] + t * cx
 
 171             ty = (1 - t) * v[1] + t * cy
 
 172             corner_text = str(triangle.index + 1) + corners_labels[i]
 
 173             self.backend.draw_centered_text(tx, ty, corner_text, text_theta, color)
 
 175     def draw_plan_template(self):
 
 176         for hexagon in self.hexaflexagon.hexagons:
 
 177             for triangle in hexagon.triangles:
 
 178                 x, y = self.get_triangle_center_in_plan(triangle)
 
 179                 theta = triangle.get_angle_in_plan()
 
 180                 self.draw_triangle_template(triangle, x, y, theta)
 
 182     def draw_template(self):
 
 183         for hexagon in self.hexaflexagon.hexagons:
 
 184             self.draw_hexagon_template(hexagon)
 
 186         self.draw_plan_template()