easy_geometry 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ module EasyGeometry
2
+ module D2
3
+ # A Ray is a semi-line in the space with a source point and a direction.
4
+ class Ray < LinearEntity
5
+
6
+ # Is other GeometryEntity contained in this Ray?
7
+ #
8
+ # Parameters:
9
+ # GeometryEntity
10
+ #
11
+ # Returns:
12
+ # true if `other` is on this Line.
13
+ # false otherwise.
14
+ #
15
+ def contains?(other)
16
+ other = Point.new(other[0], other[1]) if other.is_a?(Array)
17
+
18
+ if other.is_a?(Point)
19
+ if Point.is_collinear?(other, self.p1, self.p2)
20
+ # if we're in the direction of the ray, our
21
+ # direction vector dot the ray's direction vector
22
+ # should be non-negative
23
+ return (self.p2 - self.p1).dot(other - self.p1) >= 0
24
+ end
25
+ end
26
+
27
+ if other.is_a?(Ray)
28
+ if Point.is_collinear?(self.p1, self.p2, other.p1, other.p2)
29
+ return (self.p2 - self.p1).dot(other.p2 - other.p1) > 0
30
+ end
31
+ end
32
+
33
+ if other.is_a?(Segment)
34
+ return true if self.contains?(other.p1) && self.contains?(other.p2)
35
+ end
36
+
37
+ return false
38
+ end
39
+
40
+ # Finds the shortest distance between the ray and a point.
41
+ #
42
+ # Raises
43
+ # ======
44
+ # TypeError is raised if `other` is not a Point
45
+ def distance(other)
46
+ raise TypeError, "Distance between Ray and #{ other.class } is not defined" unless other.is_a?(Point)
47
+
48
+ return 0 if self.contains?(other)
49
+
50
+ proj = Line.new(self.p1, self.p2).projection_point(other)
51
+ if self.contains?(proj)
52
+ return (other - proj).abs
53
+ end
54
+
55
+ (other - self.source).abs
56
+ end
57
+
58
+ # Returns True if self and other are the same mathematical entities
59
+ def ==(other)
60
+ return false unless other.is_a?(Ray)
61
+ self.source == other.source && self.contains?(other.p2)
62
+ end
63
+
64
+ # The point from which the ray emanates.
65
+ def source
66
+ self.p1
67
+ end
68
+
69
+ # The x direction of the ray.
70
+ #
71
+ # Returns:
72
+ # Positive infinity if the ray points in the positive x direction,
73
+ # negative infinity if the ray points in the negative x direction,
74
+ # or 0 if the ray is vertical.
75
+ def xdirection
76
+ return @xdirection if defined?(@xdirection)
77
+
78
+ if self.p1.x < self.p2.x
79
+ @xdirection = BigDecimal('Infinity')
80
+ elsif self.p1.x == self.p2.x
81
+ @xdirection = 0
82
+ else
83
+ @xdirection = -BigDecimal('Infinity')
84
+ end
85
+
86
+ @xdirection
87
+ end
88
+
89
+ # The y direction of the ray.
90
+ #
91
+ # Returns:
92
+ # Positive infinity if the ray points in the positive y direction,
93
+ # negative infinity if the ray points in the negative y direction,
94
+ # or 0 if the ray is vertical.
95
+ def ydirection
96
+ return @ydirection if defined?(@ydirection)
97
+
98
+ if self.p1.y < self.p2.y
99
+ @ydirection = BigDecimal('Infinity')
100
+ elsif self.p1.y == self.p2.y
101
+ @ydirection = 0
102
+ else
103
+ @ydirection = -BigDecimal('Infinity')
104
+ end
105
+
106
+ @ydirection
107
+ end
108
+
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,119 @@
1
+ module EasyGeometry
2
+ module D2
3
+ # A segment in a 2-dimensional Euclidean space.
4
+ class Segment < LinearEntity
5
+
6
+ # Is the other GeometryEntity contained within this Segment?
7
+ #
8
+ # Parameters:
9
+ # GeometryEntity
10
+ #
11
+ # Returns:
12
+ # true if `other` is in this Segment.
13
+ # false otherwise.
14
+ #
15
+ def contains?(other)
16
+ other = Point.new(other[0], other[1]) if other.is_a?(Array)
17
+
18
+ if other.is_a?(Point)
19
+ if Point.is_collinear?(other, self.p1, self.p2)
20
+ # if it is collinear and is in the bounding box of the
21
+ # segment then it must be on the segment
22
+ vert = (1/self.slope).zero?
23
+
24
+ if vert
25
+ return (self.p1.y - other.y) * (self.p2.y - other.y) <= 0
26
+ end
27
+
28
+ return (self.p1.x - other.x) * (self.p2.x - other.x) <= 0
29
+ end
30
+ end
31
+
32
+ if other.is_a?(Segment)
33
+ return self.contains?(other.p1) && self.contains?(other.p2)
34
+ end
35
+
36
+ return false
37
+ end
38
+
39
+ # Returns True if self and other are the same mathematical entities
40
+ def ==(other)
41
+ return false unless other.is_a?(Segment)
42
+ [p1, p2].sort_by {|p| [p.x, p.y]} == [other.p1, other.p2].sort_by {|p| [p.x, p.y]}
43
+ end
44
+
45
+ def <=>(other)
46
+ return self.p2 <=> other.p2 if self.p1 == other.p1
47
+ self.p1 <=> other.p1
48
+ end
49
+
50
+ # Finds the shortest distance between a line segment and a point.
51
+ #
52
+ # Parameters:
53
+ # Point
54
+ #
55
+ # Returns:
56
+ # Number
57
+ #
58
+ # Raises
59
+ # ======
60
+ # TypeError is raised if `other` is not a Point
61
+ def distance(other)
62
+ other = Point.new(other[0], other[1]) if other.is_a?(Array)
63
+ raise TypeError, "Distance between Segment and #{ other.class } is not defined" unless other.is_a?(Point)
64
+
65
+ vp1 = other - self.p1
66
+ vp2 = other - self.p2
67
+
68
+ dot_prod_sign_1 = self.direction.to_point.dot(vp1) >= 0
69
+ dot_prod_sign_2 = self.direction.to_point.dot(vp2) <= 0
70
+
71
+ if dot_prod_sign_1 && dot_prod_sign_2
72
+ return Line.new(self.p1, self.p2).distance(other)
73
+ end
74
+
75
+ if dot_prod_sign_1 && !dot_prod_sign_2
76
+ return vp2.abs
77
+ end
78
+
79
+ if !dot_prod_sign_1 && dot_prod_sign_2
80
+ return vp1.abs
81
+ end
82
+ end
83
+
84
+ # The length of the line segment.
85
+ def length
86
+ @length ||= p1.distance(p2)
87
+ end
88
+
89
+ # The midpoint of the line segment.
90
+ def midpoint
91
+ @midpoint ||= p1.midpoint(p2)
92
+ end
93
+
94
+ # The perpendicular bisector of this segment.
95
+ # If no point is specified or the point specified is not on the
96
+ # bisector then the bisector is returned as a Line.
97
+ # Otherwise a Segment is returned that joins the point specified and the
98
+ # intersection of the bisector and the segment.
99
+ #
100
+ # Parameters:
101
+ # Point
102
+ #
103
+ # Returns:
104
+ # Line or Segment
105
+ #
106
+ def perpendicular_bisector(point=nil)
107
+ l = self.perpendicular_line(self.midpoint)
108
+
109
+ if !point.nil?
110
+ point = Point.new(point[0], point[1]) if point.is_a?(Array)
111
+ raise TypeError, "This method is not defined for #{ point.class }" unless point.is_a?(Point)
112
+ return Segment.new(point, self.midpoint) if l.contains?(point)
113
+ end
114
+
115
+ return l
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,375 @@
1
+ module EasyGeometry
2
+ module D2
3
+ # A polygon with three vertices and three sides.
4
+ class Triangle < Polygon
5
+ attr_reader :vertices
6
+
7
+ EQUITY_TOLERANCE = 0.0000000000001
8
+
9
+ def initialize(*args)
10
+ @vertices = preprocessing_args(args)
11
+ remove_consecutive_duplicates
12
+ remove_collinear_points
13
+ end
14
+
15
+ # Is another triangle similar to this one.
16
+ #
17
+ # Parameters:
18
+ # Triangle
19
+ #
20
+ # Returns:
21
+ # bool
22
+ #
23
+ def is_similar?(other)
24
+ return false unless other.is_a?(Triangle)
25
+
26
+ s1_1, s1_2, s1_3 = self.sides.map {|side| side.length}
27
+ s2 = other.sides.map {|side| side.length}
28
+
29
+ are_similar?(s1_1, s1_2, s1_3, *s2) ||
30
+ are_similar?(s1_1, s1_3, s1_2, *s2) ||
31
+ are_similar?(s1_2, s1_1, s1_3, *s2) ||
32
+ are_similar?(s1_2, s1_3, s1_1, *s2) ||
33
+ are_similar?(s1_3, s1_1, s1_2, *s2) ||
34
+ are_similar?(s1_3, s1_2, s1_1, *s2)
35
+ end
36
+
37
+ # Are all the sides the same length?
38
+ # Precision - 10e-13
39
+ #
40
+ # Returns:
41
+ # bool
42
+ #
43
+ def is_equilateral?
44
+ lengths = self.sides.map { |side| side.length }
45
+ lengths = lengths.map { |l| l - lengths.first }
46
+ return lengths.reject { |l| l.abs < EQUITY_TOLERANCE }.length == 0
47
+ end
48
+
49
+ # Are two or more of the sides the same length?
50
+ #
51
+ # Returns:
52
+ # bool
53
+ #
54
+ def is_isosceles?
55
+ has_dups(self.sides.map { |side| side.length })
56
+ end
57
+
58
+ # Are all the sides of the triangle of different lengths?
59
+ #
60
+ # Returns:
61
+ # bool
62
+ #
63
+ def is_scalene?
64
+ !has_dups(self.sides.map { |side| side.length })
65
+ end
66
+
67
+ # Is the triangle right-angled.
68
+ #
69
+ # Returns:
70
+ # bool
71
+ #
72
+ def is_right?
73
+ s = self.sides
74
+
75
+ s[0].perpendicular_to?(s[1]) ||
76
+ s[1].perpendicular_to?(s[2]) ||
77
+ s[0].perpendicular_to?(s[2])
78
+ end
79
+
80
+ # The altitudes of the triangle.
81
+ #
82
+ # An altitude of a triangle is a segment through a vertex,
83
+ # perpendicular to the opposite side, with length being the
84
+ # height of the vertex measured from the line containing the side.
85
+ #
86
+ # Returns:
87
+ # Hash (The hash consists of keys which are vertices and values
88
+ # which are Segments.)
89
+ #
90
+ def altitudes
91
+ return @altitudes if defined?(@altitudes)
92
+
93
+ @altitudes = {
94
+ self.vertices[0] => self.sides[1].perpendicular_segment(self.vertices[0]),
95
+ self.vertices[1] => self.sides[2].perpendicular_segment(self.vertices[1]),
96
+ self.vertices[2] => self.sides[0].perpendicular_segment(self.vertices[2])
97
+ }
98
+
99
+ @altitudes
100
+ end
101
+
102
+ # The orthocenter of the triangle.
103
+ #
104
+ # The orthocenter is the intersection of the altitudes of a triangle.
105
+ # It may lie inside, outside or on the triangle.
106
+ #
107
+ # Returns:
108
+ # Point
109
+ #
110
+ def orthocenter
111
+ return @orthocenter if defined?(@orthocenter)
112
+
113
+ a = self.altitudes
114
+ a1 = a[self.vertices[0]]; a2 = a[self.vertices[1]]
115
+
116
+ l1 = Line.new(a1.p1, a1.p2)
117
+ l2 = Line.new(a2.p1, a2.p2)
118
+
119
+ @orthocenter = l1.intersection(l2)[0]
120
+ @orthocenter
121
+ end
122
+
123
+ # The circumcenter of the triangle
124
+ #
125
+ # The circumcenter is the center of the circumcircle.
126
+ #
127
+ # Returns:
128
+ # Point or nil
129
+ #
130
+ def circumcenter
131
+ return @circumcenter if defined?(@circumcenter)
132
+
133
+ a, b, c = self.sides.map { |side| side.perpendicular_bisector }
134
+
135
+ @circumcenter = a.intersection(b)[0]
136
+ @circumcenter
137
+ end
138
+
139
+ # The radius of the circumcircle of the triangle.
140
+ #
141
+ # Returns:
142
+ # int
143
+ #
144
+ def circumradius
145
+ @circumradius ||= self.vertices[0].distance(self.circumcenter)
146
+ end
147
+
148
+ # The circle which passes through the three vertices of the triangle.
149
+ #
150
+ # Returns:
151
+ # Circle
152
+ #
153
+ def circumcircle
154
+ # Circle.new(self.circumcenter, self.circumradius)
155
+ end
156
+
157
+ # The angle bisectors of the triangle.
158
+ #
159
+ # An angle bisector of a triangle is a straight line through a vertex
160
+ # which cuts the corresponding angle in half.
161
+ #
162
+ # Returns:
163
+ # Hash (each key is a vertex (Point) and each value is the corresponding
164
+ # bisector (Segment).)
165
+ #
166
+ def bisectors
167
+ s = self.sides.map { |side| Line.new(side.p1, side.p2) }
168
+ c = self.incenter
169
+
170
+ inter1 = Line.new(self.vertices[0], c).intersection(s[1]).first
171
+ inter2 = Line.new(self.vertices[1], c).intersection(s[2]).first
172
+ inter3 = Line.new(self.vertices[2], c).intersection(s[0]).first
173
+
174
+ {
175
+ self.vertices[0] => Segment.new(self.vertices[0], inter1),
176
+ self.vertices[1] => Segment.new(self.vertices[1], inter2),
177
+ self.vertices[2] => Segment.new(self.vertices[2], inter3),
178
+ }
179
+ end
180
+
181
+ # The center of the incircle.
182
+ #
183
+ # The incircle is the circle which lies inside the triangle and touches
184
+ # all three sides.
185
+ #
186
+ # Returns:
187
+ # Point
188
+ #
189
+ def incenter
190
+ return @incenter if defined?(@incenter)
191
+
192
+ s = self.sides
193
+ l = [1, 2, 0].map { |i| s[i].length }
194
+ p = l.sum
195
+
196
+ x_arr = self.vertices.map { |v| v.x / p }
197
+ y_arr = self.vertices.map { |v| v.y / p }
198
+
199
+ x = l[0] * x_arr[0] + l[1] * x_arr[1] + l[2] * x_arr[2]
200
+ y = l[0] * y_arr[0] + l[1] * y_arr[1] + l[2] * y_arr[2]
201
+
202
+ @incenter = Point.new(x, y)
203
+ @incenter
204
+ end
205
+
206
+ # The radius of the incircle.
207
+ #
208
+ # Returns:
209
+ # int
210
+ #
211
+ def inradius
212
+ @inradius ||= 2 * self.area / self.perimeter
213
+ end
214
+
215
+ # The incircle of the triangle.
216
+ #
217
+ # The incircle is the circle which lies inside the triangle and touches
218
+ # all three sides.
219
+ #
220
+ # Returns:
221
+ # Circle
222
+ #
223
+ def incircle
224
+ # Circle.new(self.incenter, self.inradius)
225
+ end
226
+
227
+ # The radius of excircles of a triangle.
228
+ #
229
+ # An excircle of the triangle is a circle lying outside the triangle,
230
+ # tangent to one of its sides and tangent to the extensions of the
231
+ # other two.
232
+ #
233
+ # Returns:
234
+ # Hash
235
+ #
236
+ def exradii
237
+ return @exradii if defined?(@exradii)
238
+ a = self.sides[0].length
239
+ b = self.sides[1].length
240
+ c = self.sides[2].length
241
+ s = (a + b + c) / 2
242
+ area = self.area
243
+
244
+ @exradii = {
245
+ self.sides[0] => area / (s - a),
246
+ self.sides[1] => area / (s - b),
247
+ self.sides[2] => area / (s - c)
248
+ }
249
+ @exradii
250
+ end
251
+
252
+ # The medians of the triangle.
253
+ #
254
+ # A median of a triangle is a straight line through a vertex and the
255
+ # midpoint of the opposite side, and divides the triangle into two
256
+ # equal areas.
257
+ #
258
+ # Returns:
259
+ # Hash (each key is a vertex (Point) and each value is the median (Segment)
260
+ # at that point.)
261
+ #
262
+ def medians
263
+ @medians ||= {
264
+ self.vertices[0] => Segment.new(self.vertices[0], self.sides[1].midpoint),
265
+ self.vertices[1] => Segment.new(self.vertices[1], self.sides[2].midpoint),
266
+ self.vertices[2] => Segment.new(self.vertices[2], self.sides[0].midpoint)
267
+ }
268
+ end
269
+
270
+ # The medial triangle of the triangle.
271
+ # The triangle which is formed from the midpoints of the three sides.
272
+ #
273
+ # Returns:
274
+ # Triangle
275
+ #
276
+ def medial
277
+ @medial ||= Triangle.new(
278
+ self.sides[0].midpoint,
279
+ self.sides[1].midpoint,
280
+ self.sides[2].midpoint
281
+ )
282
+ end
283
+
284
+ # The nine-point circle of the triangle.
285
+ #
286
+ # Nine-point circle is the circumcircle of the medial triangle, which
287
+ # passes through the feet of altitudes and the middle points of segments
288
+ # connecting the vertices and the orthocenter.
289
+ #
290
+ # Returns:
291
+ # Circle
292
+ #
293
+ def nine_point_circle
294
+ # Circle.new(*self.medial.vertices)
295
+ end
296
+
297
+ # The Euler line of the triangle.
298
+ # The line which passes through circumcenter, centroid and orthocenter.
299
+ #
300
+ # Returns:
301
+ # Line (or Point for equilateral triangles in which case all
302
+ # centers coincide)
303
+ #
304
+ def eulerline
305
+ return self.orthocenter if self.is_equilateral?
306
+ Line.new(self.orthocenter, self.circumcenter)
307
+ end
308
+
309
+ private
310
+
311
+ def has_dups(arr)
312
+ (0...arr.length).each do |i|
313
+ return true if (arr[i] - arr[i - 1]).abs < EQUITY_TOLERANCE
314
+ end
315
+
316
+ return false
317
+ end
318
+
319
+ def are_similar?(u1, u2, u3, v1, v2, v3)
320
+ e1 = u1 / v1
321
+ e2 = u2 / v2
322
+ e3 = u3 / v3
323
+
324
+ e1 == e2 && e2 == e3
325
+ end
326
+
327
+ # preprocessing_args - convert coordinates to points if necessary.
328
+ def preprocessing_args(args)
329
+ args.map do |v|
330
+ if v.is_a?(Array) && v.length == 2
331
+ Point.new(*v)
332
+ elsif v.is_a?(Point)
333
+ v
334
+ else
335
+ raise TypeError, "Arguments should be arrays with coordinates or Points."
336
+ end
337
+ end
338
+ end
339
+
340
+ def remove_consecutive_duplicates
341
+ nodup = []
342
+ @vertices.each do |p|
343
+ next if !nodup.empty? && p == nodup[-1]
344
+ nodup << p
345
+ end
346
+
347
+ if nodup.length > 1 && nodup[-1] == nodup[0]
348
+ nodup.pop # last point was same as first
349
+ end
350
+
351
+ @vertices = nodup
352
+ validate
353
+ end
354
+
355
+ def remove_collinear_points
356
+ i = 0
357
+ while i < vertices.length
358
+ a, b, c = vertices[i], vertices[i - 1], vertices[i - 2]
359
+ if Point.is_collinear?(a, b, c)
360
+ vertices.delete_at(i - 1)
361
+ vertices.delete_at(i - 2) if a == c
362
+ else
363
+ i += 1
364
+ end
365
+ end
366
+
367
+ validate
368
+ end
369
+
370
+ def validate
371
+ raise ArgumentError, 'Triangle instantiates with three points' if vertices.length != 3
372
+ end
373
+ end
374
+ end
375
+ end