svg_hexaflexagon_editor: draw also the backfaces
[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         # The offset of the backfaces relative to the first hexagon
47         self.backfaces_offsets = (0,
48                                   (self.hexagon_radius + self.x_border) * 2)
49
50         self.hexagons_color_map = [(1, 0, 0), (0, 1, 0), (0, 0, 1)]
51
52     def _init_centers(self):
53         # Preallocate the lists to be able to access them by indices in the
54         # loops below.
55         self.hexagons_centers = [None for h in self.hexaflexagon.hexagons]
56         self.triangles_centers = [[None for t in h.triangles] for h in self.hexaflexagon.hexagons]
57
58         cy = self.backend.height - (self.hexagon_radius + self.x_border)
59         for hexagon in self.hexaflexagon.hexagons:
60             cx = self.x_border + self.hexagon_radius + (2 * self.hexagon_radius + self.x_border) * hexagon.index
61             self.hexagons_centers[hexagon.index] = (cx, cy)
62
63             triangles_centers = self.backend.get_regular_polygon(cx, cy, 6, self.triangle_radius, pi / 6)
64             for triangle in hexagon.triangles:
65                 self.triangles_centers[hexagon.index][triangle.index] = triangles_centers[triangle.index]
66
67     def get_hexagon_center(self, hexagon):
68         return self.hexagons_centers[hexagon.index]
69
70     def get_triangle_center(self, triangle):
71         return self.triangles_centers[triangle.hexagon.index][triangle.index]
72
73     def get_triangle_center_in_backfaces(self, triangle):
74         """Get the center of this triangle but in the backface"""
75         x0, y0 = self.backfaces_offsets
76
77         backface_triangle_index = triangle.get_backface_index()
78         x, y = self.triangles_centers[triangle.hexagon.index][backface_triangle_index]
79
80         return x0 + x, y0 + y
81
82     def get_triangle_center_in_plan(self, triangle):
83         x0, y0 = self.plan_origin
84         i, j = self.hexaflexagon.get_triangle_plan_position(triangle)
85         x, y = triangle.calc_plan_coordinates(self.triangle_radius, i, j)
86         return x0 + x, y0 + y
87
88     def get_triangle_verts(self, triangle):
89         cx, cy = self.get_triangle_center(triangle)
90         theta = triangle.get_angle_in_hexagon()
91         verts = self.backend.get_regular_polygon(cx, cy, 3, self.triangle_radius, theta)
92         return verts
93
94     def get_triangle_transform(self, triangle):
95         """Calculate the transformation matrix from a triangle in an hexagon to
96         the correspondent triangle in the plan.
97
98         Return the matrix as a list of values sorted in row-major order."""
99
100         src_x, src_y = self.get_triangle_center(triangle)
101         dest_x, dest_y = self.get_triangle_center_in_plan(triangle)
102         theta = triangle.get_angle_in_plan_relative_to_hexagon()
103
104         return self.backend.calc_rotate_translate_transform(src_x, src_y,
105                                                             dest_x, dest_y, theta)
106
107     def get_triangle_backfaces_transform(self, triangle):
108         """Calculate the transformation matrix from a triangle in an hexagon to
109         the correspondent triangle in a backface.
110
111         Return the matrix as a list of values sorted in row-major order."""
112         src_x, src_y = self.get_triangle_center(triangle)
113         dest_x, dest_y = self.get_triangle_center_in_backfaces(triangle)
114         theta = triangle.get_angle_in_backface_relative_to_hexagon()
115
116         return self.backend.calc_rotate_translate_transform(src_x, src_y,
117                                                             dest_x, dest_y, theta)
118
119     def draw_hexagon_template(self, hexagon):
120         for triangle in hexagon.triangles:
121             cx, cy = self.get_triangle_center(triangle)
122             theta = triangle.get_angle_in_hexagon()
123             self.draw_triangle_template(triangle, cx, cy, theta)
124
125     def draw_triangle_template(self, triangle, cx, cy, theta):
126         radius = self.triangle_radius
127         color = self.hexagons_color_map[triangle.hexagon.index]
128
129         tverts = self.backend.draw_regular_polygon(cx, cy, 3, radius, theta, color)
130
131         self.backend.draw_apothem_star(cx, cy, 3, radius, theta, color)
132
133         # Because of how draw_regular_polygon() is implemented, triangles are
134         # drawn by default with the base on the top, so the text need to be
135         # rotated by 180 to look like it is in the same orientation as
136         # a triangle with the base on the bottom.
137         text_theta = pi - theta
138
139         # Draw the text closer to the vertices of the element
140         t = 0.3
141
142         corners_labels = "ABC"
143         for i, v in enumerate(tverts):
144             tx = (1 - t) * v[0] + t * cx
145             ty = (1 - t) * v[1] + t * cy
146             corner_text = str(triangle.index + 1) + corners_labels[i]
147             self.backend.draw_centered_text(tx, ty, corner_text, text_theta, color)
148
149     def draw_plan_template(self):
150         for hexagon in self.hexaflexagon.hexagons:
151             for triangle in hexagon.triangles:
152                 x, y = self.get_triangle_center_in_plan(triangle)
153                 theta = triangle.get_angle_in_plan()
154                 self.draw_triangle_template(triangle, x, y, theta)
155
156     def draw_template(self):
157         for hexagon in self.hexaflexagon.hexagons:
158             self.draw_hexagon_template(hexagon)
159
160         self.draw_plan_template()