-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgeometry.py
319 lines (258 loc) · 10.7 KB
/
geometry.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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
from .items.line import line
from .items.point import point, static_point
from .items.item import item
from .maths import spread_item
from .algorithms import deep_map, flatten
from math import cos, sin, atan2, sqrt, pi, exp, log
from typing import List as _List, Tuple as _Tuple, Callable as _Callable, Any as _Any
hpi = pi / 2.
qpi = pi / 4.
tau = pi * 2.
origin = point(0., 0.)
#################################################################
#
# Min/max
def min_max(points: _List[static_point]) -> _Tuple[static_point]:
minX = min([pt.x() for pt in points])
maxX = max([pt.x() for pt in points])
minY = min([pt.y() for pt in points])
maxY = max([pt.y() for pt in points])
return static_point(minX, minY), static_point(maxX, maxY)
def select_size(items: _List[item], selector: _Callable) -> static_point:
"""
Return the selected width and height among all items.
"""
rects = [item.scene_rect() for item in flatten(items)]
return static_point(
selector([rect.width() for rect in rects]),
selector([rect.height() for rect in rects]))
def maximum_size(items: _List[item]) -> static_point:
"""
Return the maximum width and height of all items.
"""
return select_size(items, max)
def minimum_size(items: _List[item]) -> static_point:
"""
Return the maximum width and height of all items.
"""
return select_size(items, min)
#################################################################
#
# Centers
def weighted_center_of(points: _List[static_point]) -> static_point:
total = static_point()
for pt in points:
total = total + pt
return total / len(points)
def center_of(points: _List[static_point]) -> static_point:
p1, p2 = min_max(points)
return (p1 + p2) / 2.
#################################################################
#
# Bisectors
def two_lines_bisector(l1: line, l2: line) -> line:
return line(*four_points_bisector(l1.p1, l1.p2, l2.p1, l2.p2))
def four_points_bisector(l1_p1: static_point, l1_p2: static_point, l2_p1: static_point, l2_p2: static_point) -> _Tuple[point, point]:
p1 = four_points_any_intersection(l1_p1, l1_p2, l2_p1, l2_p2)
if p1 is None:
return (point((l1_p1 + l2_p1) / 2.), point((l1_p2 + l2_p2) / 2.))
a1 = two_points_angle(l1_p1, l1_p2)
a2 = two_points_angle(l2_p1, l2_p2)
bisector_angle = (a1 + a2) / 2.
d1 = point.distance(l1_p1, l1_p2)
d2 = point.distance(l2_p1, l2_p2)
d = min(d1, d2)
p2 = point(p1 + static_point(cos(bisector_angle) * d, sin(bisector_angle) * d))
return (p1, p2)
#################################################################
#
# Angles
def two_lines_angle(l1: line, l2: line) -> float:
"""
Returns the angle between the lines.
"""
return line_angle(l2) - line_angle(l1)
def line_angle(line: line) -> float:
"""
Returns the angle between the X axis and the line.
"""
return two_points_angle(line.p1, line.p2)
def four_points_angle(l1_p1: static_point, l1_p2: static_point, l2_p1: static_point, l2_p2: static_point) -> float:
"""
Returns the angle between the lines formed by the four points.
"""
return two_points_angle(l2_p1, l2_p2) - two_points_angle(l1_p1, l1_p2)
def two_points_angle(p1: static_point, p2: static_point) -> float:
"""
Returns the angle in degrees between the X axis and the line formed by the two points.
"""
delta = p2 - p1
return atan2(delta.y(), delta.x())
#################################################################
#
# Angles to align lines
def straighten_angle(p1: point, p2: point, horizontal) -> float:
"""
Returns the angle by which the points must be rotated
to aligned the line to be horizontal or vertical.
"""
extra = 0. if horizontal else hpi
angle = -two_points_angle(p1, p2) + extra
if angle >= pi:
angle -= pi
if angle <= -pi:
angle += pi
return angle
def horizontal_angle(p1: point, p2: point) -> float:
"""
Returns the angle by which the points must be rotated
to aligned the line to be horizontal.
"""
return straighten_angle(p1, p2, True)
def vertical_angle(p1: point, p2: point) -> float:
"""
Returns the angle by which the points must be rotated
to aligned the line to be vertical.
"""
return straighten_angle(p1, p2, False)
#################################################################
#
# Dot products.
def two_points_dot(p1: static_point, p2: static_point) -> float:
return p1.x() * p2.x() + p1.y() * p2.y()
def two_points_sin_dot(p1: static_point, p2: static_point) -> float:
return p1.y() * p2.x() - p1.x() * p2.y()
#################################################################
#
# Distances
def point_to_line_distance(pt: static_point, line: line) -> float:
return point_to_two_points_distance(pt, line.p1, line.p2)
def point_to_two_points_distance(pt: static_point, p1: static_point, p2: static_point) -> float:
delta = p2 - p1
delta_dot = two_points_dot(delta, delta)
if not delta_dot:
return point.distance(pt, p1)
t = two_points_dot(pt - p1, delta) / delta_dot
if -epsilon < t < 1. + epsilon:
ox = p1.x() + t * (p2.x() - p1.x())
oy = p1.y() + t * (p2.y() - p1.y())
return sqrt((pt.x() - ox) ** 2 + (pt.y() - oy) ** 2)
elif t < 0.:
return point.distance(pt, p1)
else:
return point.distance(pt, p2)
#################################################################
#
# Projections
def two_points_convex_sum(p1: static_point, p2: static_point, t: float) -> point:
return point(p1 * (1. - t) + p2 * t)
def mid_point(p1: static_point, p2: static_point) -> point:
return two_points_convex_sum(p1, p2, 0.5)
def point_to_line_projection(pt: static_point, line: line) -> point:
return point_to_two_points_projection(pt, line.p1, line.p2)
def point_to_two_points_projection(pt: static_point, p1: static_point, p2: static_point) -> point:
delta = p2 - p1
delta_dot = two_points_dot(delta, delta)
if not delta_dot:
return p1
t = two_points_dot(pt - p1, delta) / delta_dot
return two_points_convex_sum(p1 , p2, t)
def mirror_point_on_line(mirrored_point: point, mirror_line: line, ratio: float = 1.) -> static_point:
# The following is equivalent to these steps, but with each equation
# folded into the next:
#
# delta = org_point - pt_on_line
# mirror = pt_on_line - delta
# delta = org_point - mirror
# position = org_point - delta * ratio
# mirrored_point.set_absolute_point(position)
pt_on_line = point_to_line_projection(mirrored_point, mirror_line)
return mirrored_point - (mirrored_point - pt_on_line) * (2 * ratio)
#################################################################
#
# Intersections
epsilon = 0.00001
def two_lines_intersection(l1: line, l2: line) -> point:
"""
Returns the intersection point of two lines.
Returns None if parallel or if it ends up outside of the lines.
"""
return four_points_intersection(l1.p1, l1.p2, l2.p1, l2.p2)
def two_lines_any_intersection(l1: line, l2: line) -> point:
"""
Returns the intersection point of two lines, even if outside the lines.
Returns None if parallel.
"""
return four_points_any_intersection(l1.p1, l1.p2, l2.p1, l2.p2)
def two_lines_intersection_within(l1: line, l2: line) -> point:
"""
Returns the intersection point of two lines, but not if on end-points.
Returns None if parallel.
"""
return four_points_intersection_within(l1.p1, l1.p2, l2.p1, l2.p2)
def four_points_intersection(l1_p1: static_point, l1_p2: static_point, l2_p1: static_point, l2_p2: static_point) -> point:
"""
Returns the intersection point of the two lines formed by the four points.
Returns None if parallel or if it ends up outside of the lines.
"""
params = _stay_on_units(_interesection_params(l1_p1, l1_p2, l2_p1, l2_p2))
if params is None:
return params
return two_points_convex_sum(l1_p1 , l1_p2, params.x())
def four_points_any_intersection(l1_p1: static_point, l1_p2: static_point, l2_p1: static_point, l2_p2: static_point) -> point:
"""
Returns the intersection point of the two lines formed by the four points, even if outside the lines.
Returns None if parallel.
"""
params = _interesection_params(l1_p1, l1_p2, l2_p1, l2_p2)
if params is None:
return params
return two_points_convex_sum(l1_p1 , l1_p2, params.x())
def four_points_intersection_within(l1_p1: static_point, l1_p2: static_point, l2_p1: static_point, l2_p2: static_point) -> point:
"""
Returns the intersection point of the two lines formed by the four points, but not if on end-points.
Returns None if parallel.
"""
params = _stay_on_units(_interesection_params(l1_p1, l1_p2, l2_p1, l2_p2))
if params is None:
return params
px = params.x()
py = params.y()
if px < epsilon or px > (1. - epsilon) or py < epsilon or py > (1. - epsilon):
return None
return two_points_convex_sum(l1_p1 , l1_p2, px)
def _interesection_params(l1_p1: static_point, l1_p2: static_point, l2_p1: static_point, l2_p2: static_point) -> static_point:
l1_p1_x = l1_p1.x(); l1_p1_y = l1_p1.y()
l1_p2_x = l1_p2.x(); l1_p2_y = l1_p2.y()
l2_p1_x = l2_p1.x(); l2_p1_y = l2_p1.y()
l2_p2_x = l2_p2.x(); l2_p2_y = l2_p2.y()
l1_dx = l1_p2_x - l1_p1_x; l1_dy = l1_p2_y - l1_p1_y
l2_dx = l2_p2_x - l2_p1_x; l2_dy = l2_p2_y - l2_p1_y
# Check parallel
det = (l1_dx * l2_dy) - (l1_dy * l2_dx)
if -epsilon < det < epsilon:
return None
l1_param = -((l1_p1_x * l2_dy) + l2_p1_x * (l1_p1_y - l2_p2_y) + l2_p2_x * (l2_p1_y - l1_p1_y)) / det
l2_param = ((l1_p1_x * (l2_p1_y - l1_p2_y)) + l1_p2_x * (l1_p1_y - l2_p1_y) + l2_p1_x * l1_dy) / det
return static_point(l1_param, l2_param)
def _stay_on_units(params: static_point) -> static_point:
"""
Coerce the params to be invalid if not both in the range [0., 1.].
"""
l1_param = params.x()
if l1_param < -epsilon or l1_param > 1.0 + epsilon:
return None
l2_param = params.y()
if l2_param < -epsilon or l2_param > 1.0 + epsilon:
return None
return params
#################################################################
#
# Points spreads
def create_spread_of_points(spread: _List[_List[spread_item]], top: point, row_offset: static_point, column_offset: static_point) -> _List[_List[point]]:
"""
Creates a spread (list of lists) of points in an arrangement starting at top.
Each row is offset from the preceeding row by the row_offset.
Each point in the row is offset from the preceeding point by the column_offset.
"""
return deep_map(lambda item: point(top + row_offset * item.row + column_offset * item.column), spread)