4bb029bd183762666951b6c56fcabd87807f1eea
[flexagon-toolkit.git] / src / flexagon / hexaflexagon_diagram.py
1 #!/usr/bin/env python
2 #
3 # A class to draw hexaflexagons
4 #
5 # Copyright (C) 2018  Antonio Ospite <ao2@ao2.it>
6 #
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.
11 #
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.
16 #
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/>.
19
20 from math import cos, pi
21 from .trihexaflexagon import TriHexaflexagon
22
23
24 class HexaflexagonDiagram(object):
25     def __init__(self, x_border, backend=None):
26         self.x_border = x_border
27         self.backend = backend
28
29         self.hexaflexagon = TriHexaflexagon()
30
31         num_hexagons = len(self.hexaflexagon.hexagons)
32         self.hexagon_radius = (self.backend.width - (x_border * (num_hexagons + 1))) / (num_hexagons * 2)
33
34         # The hexagon apothem is the triangle height
35         hexagon_apothem = self.hexagon_radius * cos(pi / 6.)
36
37         # the triangle radius is 2/3 of its height
38         self.triangle_radius = hexagon_apothem * 2. / 3.
39
40         self._init_centers()
41
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.)
45
46         self.hexagons_color_map = [(1, 0, 0), (0, 1, 0), (0, 0, 1)]
47
48     def _init_centers(self):
49         # Preallocate the lists to be able to access them by indices in the
50         # loops below.
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]
53
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)
58
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]
62
63     def get_hexagon_center(self, hexagon):
64         return self.hexagons_centers[hexagon.index]
65
66     def get_triangle_center(self, triangle):
67         return self.triangles_centers[triangle.hexagon.index][triangle.index]
68
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)
73         return x0 + x, y0 + y
74
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)
79         return verts
80
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.
84
85         Return the matrix as a list of values sorted in row-major order."""
86
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()
90
91         return self.backend.calc_rotate_translate_transform(src_x, src_y,
92                                                             dest_x, dest_y, theta)
93
94     def draw_hexagon_template(self, hexagon):
95         for triangle in hexagon.triangles:
96             cx, cy = self.get_triangle_center(triangle)
97             theta = triangle.get_angle_in_hexagon()
98             self.draw_triangle_template(triangle, cx, cy, theta)
99
100     def draw_triangle_template(self, triangle, cx, cy, theta):
101         radius = self.triangle_radius
102         color = self.hexagons_color_map[triangle.hexagon.index]
103
104         tverts = self.backend.draw_regular_polygon(cx, cy, 3, radius, theta, color)
105
106         self.backend.draw_apothem_star(cx, cy, 3, radius, theta, color)
107
108         # Because of how draw_regular_polygon() is implemented, triangles are
109         # drawn by default with the base on the top, so the text need to be
110         # rotated by 180 to look like it is in the same orientation as
111         # a triangle with the base on the bottom.
112         text_theta = pi - theta
113
114         # Draw the text closer to the vertices of the element
115         t = 0.3
116
117         corners_labels = "ABC"
118         for i, v in enumerate(tverts):
119             tx = (1 - t) * v[0] + t * cx
120             ty = (1 - t) * v[1] + t * cy
121             corner_text = str(triangle.index + 1) + corners_labels[i]
122             self.backend.draw_centered_text(tx, ty, corner_text, text_theta, color)
123
124     def draw_plan_template(self):
125         for hexagon in self.hexaflexagon.hexagons:
126             for triangle in hexagon.triangles:
127                 x, y = self.get_triangle_center_in_plan(triangle)
128                 theta = triangle.get_angle_in_plan()
129                 self.draw_triangle_template(triangle, x, y, theta)
130
131     def draw_template(self):
132         for hexagon in self.hexaflexagon.hexagons:
133             self.draw_hexagon_template(hexagon)
134
135         self.draw_plan_template()