1a1cdff19ec2086d4a3c113263d66b8971da4369
[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 sin, 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         # The transformation from a triangle in the hexagon to the correspondent
92         # triangle in the plan is composed by these steps:
93         #
94         #   1. rotate by 'theta' around (src_x, src_y);
95         #   2. move to (dest_x, dest_y).
96         #
97         # Step 1 can be expressed by these sub-steps:
98         #
99         #  1a. translate by (-src_x, -src_y)
100         #  1b. rotate by 'theta'
101         #  1c. translate by (src_x, src_y)
102         #
103         # Step 2. can be expressed by a translation like:
104         #
105         #  2a. translate by (dest_x - src_x, dest_y - src_y)
106         #
107         # The consecutive translations 1c and 2a can be easily combined, so
108         # the final steps are:
109         #
110         #  T1 -> translate by (-src_x, -src_y)
111         #  R  -> rotate by 'theta'
112         #  T2 -> translate by (dest_x, dest_y)
113         #
114         # Using affine transformations these are expressed as:
115         #
116         #      | 1  0  -src_x |
117         # T1 = | 0  1  -src_y |
118         #      | 0  0       1 |
119         #
120         #      | cos(theta)  -sin(theta)  0 |
121         # R  = | sin(theta)   con(theta)  0 |
122         #      |          0            0  1 |
123         #
124         #      | 1  0  dest_x |
125         # T2 = | 0  1  dest_y |
126         #      | 0  0       1 |
127         #
128         # Composing these transformations into one is achieved by multiplying
129         # the matrices from right to left:
130         #
131         #   T = T2 * R * T1
132         #
133         # NOTE: To remember this think about composing functions: T2(R(T1())),
134         # the inner one is performed first.
135         #
136         # The resulting  T matrix is the one below.
137         matrix = [
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,
140                      0,           0,                                                 1
141         ]
142
143         return matrix
144
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)
150
151     def draw_triangle_template(self, triangle, cx, cy, theta):
152         radius = self.triangle_radius
153         color = self.hexagons_color_map[triangle.hexagon.index]
154
155         tverts = self.backend.draw_regular_polygon(cx, cy, 3, radius, theta, color)
156
157         self.backend.draw_apothem_star(cx, cy, 3, radius, theta, color)
158
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
164
165         # Draw the text closer to the vertices of the element
166         t = 0.3
167
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)
174
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)
181
182     def draw_template(self):
183         for hexagon in self.hexaflexagon.hexagons:
184             self.draw_hexagon_template(hexagon)
185
186         self.draw_plan_template()