-
Notifications
You must be signed in to change notification settings - Fork 0
/
symmetry.py
161 lines (130 loc) · 5.5 KB
/
symmetry.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
"""
Algorithm:
* Double the number of points, inserting additional points between vertices
* Iterate from first point to number of points / 2
== prepare shape ==
** Select the point (pf)
** Translate the shape so pf is at 0,0
** Pick the opposite point (po)
** Calculate the angle of po
** Rotate shape so po.y is at 0
== Check if mirror image of shape along X axis matches ==
** Iterate points on either side of pf towards po (pi_top, pi_bottom)
*** compare absolute value of y coordinate of pi_top and pi_bottom. If they equal, then the pair of points match
*** If all coordinates pairs match, the line between pf and po is a line of symmetry
*** stash this in our list of symmetries
"""
from copy import deepcopy
import math
from typing import Callable, List, Optional, Set
class Point:
pass
class Point:
"""
Stores a point by x and y coordinates
"""
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def __copy__(self) -> Point:
return Point(self.x, self.y)
def __str__(self) -> str:
return self.__repr__()
def __repr__(self) -> str:
return f"({self.x},{self.y})"
class Symmetry:
"""
Detects lines of symmetry for the specified shape
"""
# How close matching pixels need to be
CLOSE_ENOUGH = 5
def __init__(
self,
points: List[Point],
original_callback: Optional[Callable[[int, List[Point]], None]] = None,
translate_callback: Optional[Callable[[int, List[Point]], None]] = None,
rotate_callback: Optional[Callable[[int, List[Point]], None]] = None,
symmetry_callback: Optional[Callable[[int, Point, Point], None]] = None,
) -> None:
self._points = points
self._original_callback = original_callback
self._translate_callback = translate_callback
self._rotate_callback = rotate_callback
self._symmetry_callback = symmetry_callback
def _translate_shape(self, points: List[Point], dx: float, dy: float) -> None:
for pc in points:
pc.x -= dx
pc.y -= dy
def _get_distance(self, p1: Point, p2: Point) -> float:
return (((p2.x - p1.x) ** 2) + ((p2.y - p1.y) ** 2)) ** 0.5
def _get_angle(self, p1: Point, p2: Point) -> float:
return -math.degrees(math.atan2(p2.y - p1.y, p2.x - p1.x))
def _get_midpoint(self, p1: Point, p2: Point) -> Point:
return Point((p1.x + p2.x) / 2, (p1.y + p2.y) / 2)
def _points_really_close(self, p1: Point, p2: Point) -> bool:
return self._get_distance(p1, p2) < Symmetry.CLOSE_ENOUGH
def _rotate_point(self, p: Point, angle: float) -> None:
angle = math.radians(angle)
new_x = p.x * math.cos(angle) - p.y * math.sin(angle)
new_y = p.x * math.sin(angle) + p.y * math.cos(angle)
p.x = new_x
p.y = new_y
def _double_points(self, points: List[Point]) -> List[Point]:
doubled_points = []
previous_point = None
for point in points:
if previous_point:
doubled_points.append(self._get_midpoint(previous_point, point))
doubled_points.append(point)
previous_point = point
if previous_point:
doubled_points.append(self._get_midpoint(previous_point, points[0]))
return doubled_points
def get_lines_of_symmetry(self) -> List[List[Point]]:
points = self._points
symmetries = []
# double the number of points by inserting mid-line points
points = self._double_points(points)
length = len(points)
half_length = int(length / 2)
for index in range(half_length):
if self._original_callback:
self._original_callback(index, points)
# make a working copy of the points
points_copy = deepcopy(points)
pf = points_copy[index]
# move shape so pf is at (0,0)
self._translate_shape(points_copy, pf.x, pf.y)
# notifiy anyone who cares
if self._translate_callback:
self._translate_callback(index, points_copy)
# figure out the opposite point symmetry line
opposite_index = index + half_length
po = points_copy[opposite_index]
# Rotate shape so po.y = 0
angle = self._get_angle(pf, po)
for p in points_copy:
self._rotate_point(p, angle)
# Notify anyone who cares
if self._rotate_callback:
self._rotate_callback(index, points_copy)
# now check if other side of the symmetry line is a mirror image
symmetrical = True
for mirror_index in range(half_length):
mirror1_point = points_copy[index - mirror_index]
mirror2_point = points_copy[index + mirror_index]
if not self._points_really_close(
Point(mirror1_point.x, abs(mirror1_point.y)),
Point(mirror2_point.x, abs(mirror2_point.y)),
):
symmetrical = False
break
if symmetrical:
# Get the original symmetry points and stash
symmetry_point1 = points[index]
symmetry_point2 = points[opposite_index]
symmetries.append([symmetry_point1, symmetry_point2])
# Notify anyone who cares
if self._symmetry_callback:
self._symmetry_callback(index, symmetry_point1, symmetry_point2)
return symmetries