#!/usr/bin/env python # # An class to draw hexaflexagons # # 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, 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): for hexagon in self.hexaflexagon.hexagons: for triangle in hexagon.triangles: x, y = self.get_triangle_center_in_plan(triangle) theta = triangle.get_angle_in_plan() self.draw_triangle_template(triangle, x, y, theta) def draw_template(self): for hexagon in self.hexaflexagon.hexagons: self.draw_hexagon_template(hexagon) self.draw_plan_template()