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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c1fb6f95cfb1ee017a62c9f6ed39a3ec916fadcaf966291e239de58da7d81307
4
+ data.tar.gz: fe76ffe30725d0ff656ae85b706b5481ac1959eebe7482fdb608424762c59629
5
+ SHA512:
6
+ metadata.gz: 525e81099138a5fad5f62e165e4fba932225a5a84cccf430a3033ba6d31e17467ddaa34777eb3cb06d15c8ce2fc13ce65e62f3d9371ce2ca97102aa0dfc9bcb8
7
+ data.tar.gz: 7941f809a621cbeeeaa7b753bb1890115d0d7fbbbf5c29837f28d7ec83ea8159ea7751f8638e34b345bde8448e4136d8e39124389fc34bdb2d3fbc3ba9a9a3d6
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1 @@
1
+ 2.6.3
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+
4
+ group :development do
5
+ gem 'rspec'
6
+ end
@@ -0,0 +1,26 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.3)
5
+ rspec (3.8.0)
6
+ rspec-core (~> 3.8.0)
7
+ rspec-expectations (~> 3.8.0)
8
+ rspec-mocks (~> 3.8.0)
9
+ rspec-core (3.8.1)
10
+ rspec-support (~> 3.8.0)
11
+ rspec-expectations (3.8.4)
12
+ diff-lcs (>= 1.2.0, < 2.0)
13
+ rspec-support (~> 3.8.0)
14
+ rspec-mocks (3.8.1)
15
+ diff-lcs (>= 1.2.0, < 2.0)
16
+ rspec-support (~> 3.8.0)
17
+ rspec-support (3.8.2)
18
+
19
+ PLATFORMS
20
+ ruby
21
+
22
+ DEPENDENCIES
23
+ rspec
24
+
25
+ BUNDLED WITH
26
+ 1.16.4
data/README ADDED
@@ -0,0 +1,2 @@
1
+ # Tests
2
+ ## rspec spec/d2/polygon_spec.rb --format doc
@@ -0,0 +1,17 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "easy_geometry"
3
+ s.version = "0.1.0"
4
+ s.author = ["Henry Metlov"]
5
+ s.date = '2019-10-20'
6
+ s.homepage = "https://github.com/Metloff/easy_geometry"
7
+ s.description = "Geometric primitives and algorithms for Ruby"
8
+ s.summary = "Geometric primitives and algorithms for Ruby"
9
+ s.license = "MIT"
10
+
11
+ s.files = `git ls-files`.split($/)
12
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
13
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
14
+ s.require_paths = ["lib"]
15
+
16
+ s.add_development_dependency 'rspec'
17
+ end
@@ -0,0 +1 @@
1
+ 2.6.3
@@ -0,0 +1,12 @@
1
+ module EasyGeometry
2
+ require 'matrix'
3
+ require 'bigdecimal'
4
+ require_relative 'easy_geometry/d2/point'
5
+ require_relative 'easy_geometry/d2/vector'
6
+ require_relative 'easy_geometry/d2/linear_entity'
7
+ require_relative 'easy_geometry/d2/line'
8
+ require_relative 'easy_geometry/d2/segment'
9
+ require_relative 'easy_geometry/d2/ray'
10
+ require_relative 'easy_geometry/d2/polygon'
11
+ require_relative 'easy_geometry/d2/triangle'
12
+ end
@@ -0,0 +1,73 @@
1
+ module EasyGeometry
2
+ module D2
3
+ # An infinite line in 2-dimensional Euclidean space.
4
+ class Line < LinearEntity
5
+
6
+ #
7
+ # Parameters:
8
+ # GeometryEntity
9
+ #
10
+ # Returns:
11
+ # true if `other` is on this Line.
12
+ # false otherwise.
13
+ #
14
+ def contains?(other)
15
+ if other.is_a?(Point)
16
+ return Point.is_collinear?(other, self.p1, self.p2)
17
+ end
18
+
19
+ if other.is_a?(LinearEntity)
20
+ return Point.is_collinear?(other.p1, other.p2, self.p1, self.p2)
21
+ end
22
+
23
+ return false
24
+ end
25
+
26
+ # Finds the shortest distance between a line and a point.
27
+ #
28
+ # Raises
29
+ # ======
30
+ # TypeError is raised if `other` is not a Point
31
+ def distance(other)
32
+ other = Point.new(other[0], other[1]) if other.is_a?(Array)
33
+ raise TypeError, "Distance between Line and #{ other.class } is not defined" unless other.is_a?(Point)
34
+
35
+ return 0 if self.contains?(other)
36
+ self.perpendicular_segment(other).length
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?(Line)
42
+
43
+ Point.is_collinear?(self.p1, other.p1, self.p2, other.p2)
44
+ end
45
+
46
+ # The equation of the line: ax + by + c.
47
+ def equation
48
+ if p1.x == p2.x
49
+ return "x - #{p1.x}"
50
+ elsif p1.y == p2.y
51
+ return "#{p2.y} - p1.y"
52
+ end
53
+
54
+ "#{a}*x + #{b}*y + #{c} = 0"
55
+ end
56
+
57
+ # The coefficients 'a' for ax + by + c = 0.
58
+ def a
59
+ @a ||= self.p1.y - self.p2.y
60
+ end
61
+
62
+ # The coefficients 'b' for ax + by + c = 0.
63
+ def b
64
+ @b ||= self.p2.x - self.p1.x
65
+ end
66
+
67
+ # The coefficients 'c' for ax + by + c = 0.
68
+ def c
69
+ @c ||= self.p1.x * self.p2.y - self.p1.y * self.p2.x
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,355 @@
1
+ module EasyGeometry
2
+ module D2
3
+ # A base class for all linear entities (Line, Ray and Segment)
4
+ # in 2-dimensional Euclidean space.
5
+ class LinearEntity
6
+ attr_reader :p1, :p2
7
+
8
+ # Examples:
9
+ # LinearEntity.new(Point.new(0, 0), Point.new(1, 2))
10
+ # LinearEntity.new([0, 0], [1, 2])
11
+ def initialize(point1, point2)
12
+ @p1 = point1; @p2 = point2
13
+
14
+ check_input_points!
15
+ validate!
16
+ end
17
+
18
+ # The direction vector of the LinearEntity.
19
+ # Returns:
20
+ # Point; the ray from the origin to this point is the
21
+ # direction of `self`.
22
+ #
23
+ def direction
24
+ @direction ||= Vector.new(p2.x - p1.x, p2.y - p1.y)
25
+ end
26
+
27
+ # Return the non-reflex angle formed by rays emanating from
28
+ # the origin with directions the same as the direction vectors
29
+ # of the linear entities.
30
+ #
31
+ # From the dot product of vectors v1 and v2 it is known that:
32
+ #
33
+ # ``dot(v1, v2) = |v1|*|v2|*cos(A)``
34
+ #
35
+ # where A is the angle formed between the two vectors. We can
36
+ # get the directional vectors of the two lines and readily
37
+ # find the angle between the two using the above formula.
38
+ #
39
+ #
40
+ # Parameters:
41
+ # LinearEntity
42
+ #
43
+ # Returns:
44
+ # angle in radians
45
+ #
46
+ def angle_between(other)
47
+ raise TypeError, 'Must pass only LinearEntity objects.' unless other.is_a?(LinearEntity)
48
+
49
+ v1 = self.direction
50
+ v2 = other.direction
51
+
52
+ # Convert numerator to BigDecimal for more precision.
53
+ numerator = BigDecimal(v1.dot(v2).to_f.to_s)
54
+ denominator = v1.to_point.abs * v2.to_point.abs
55
+
56
+ return Math.acos(numerator / denominator)
57
+ end
58
+
59
+ # Are two LinearEntity parallel?
60
+ #
61
+ # Parameters:
62
+ # LinearEntity
63
+ #
64
+ # Returns:
65
+ # true if self and other LinearEntity are parallel.
66
+ # false otherwise.
67
+ #
68
+ def parallel_to?(other)
69
+ raise TypeError, 'Must pass only LinearEntity objects.' unless other.is_a?(LinearEntity)
70
+ self.direction.cross_product(other.direction) == 0
71
+ end
72
+
73
+ # Are two linear entities perpendicular?
74
+ #
75
+ # Parameters:
76
+ # LinearEntity
77
+ #
78
+ # Returns:
79
+ # true if self and other LinearEntity are perpendicular.
80
+ # false otherwise.
81
+ #
82
+ def perpendicular_to?(other)
83
+ raise TypeError, 'Must pass only LinearEntity objects.' unless other.is_a?(LinearEntity)
84
+ self.direction.dot(other.direction) == 0
85
+ end
86
+
87
+ # Are two linear entities similar?
88
+ #
89
+ # Return:
90
+ # true if self and other are contained in the same line.
91
+ #
92
+ def similar_to?(other)
93
+ raise TypeError, 'Must pass only LinearEntity objects.' unless other.is_a?(LinearEntity)
94
+
95
+ l = Line.new(p1, p2)
96
+ l.contains?(other)
97
+ end
98
+
99
+ # The intersection with another geometrical entity
100
+ #
101
+ # Parameters:
102
+ # Point or LinearEntity
103
+ #
104
+ # Returns:
105
+ # Array of geometrical entities
106
+ #
107
+ def intersection(other)
108
+ other = Point.new(other[0], other[1]) if other.is_a?(Array)
109
+
110
+ # Other is a Point.
111
+ if other.is_a?(Point)
112
+ return [other] if self.contains?(other)
113
+ return []
114
+ end
115
+
116
+ # Other is a LinearEntity
117
+ if other.is_a?(LinearEntity)
118
+ # break into cases based on whether
119
+ # the lines are parallel, non-parallel intersecting, or skew
120
+ rank = Point.affine_rank(self.p1, self.p2, other.p1, other.p2)
121
+ if rank == 1
122
+ # we're collinear
123
+ return [other] if self.is_a?(Line)
124
+ return [self] if other.is_a?(Line)
125
+
126
+ if self.is_a?(Ray) && other.is_a?(Ray)
127
+ return intersect_parallel_rays(self, other)
128
+ end
129
+
130
+ if self.is_a?(Ray) && other.is_a?(Segment)
131
+ return intersect_parallel_ray_and_segment(self, other)
132
+ end
133
+
134
+ if self.is_a?(Segment) && other.is_a?(Ray)
135
+ return intersect_parallel_ray_and_segment(other, self)
136
+ end
137
+
138
+ if self.is_a?(Segment) && other.is_a?(Segment)
139
+ return intersect_parallel_segments(self, other)
140
+ end
141
+
142
+ elsif rank == 2
143
+ # we're in the same plane
144
+ l1 = Line.new(self.p1, self.p2)
145
+ l2 = Line.new(other.p1, other.p2)
146
+
147
+ # check to see if we're parallel. If we are, we can't
148
+ # be intersecting, since the collinear case was already
149
+ # handled
150
+ return [] if l1.parallel_to?(l2)
151
+
152
+ # Use Cramers rule:
153
+ # https://en.wikipedia.org/wiki/Cramer%27s_rule
154
+ det = l1.a * l2.b - l2.a * l1.b
155
+ det = det
156
+ x = (l1.b * l2.c - l1.c * l2.b) / det
157
+ y = (l2.a * l1.c - l2.c * l1.a ) / det
158
+
159
+ intersection_point = Point.new(x, y)
160
+
161
+ # if we're both lines, we can skip a containment check
162
+ return [intersection_point] if self.is_a?(Line) && other.is_a?(Line)
163
+
164
+ if self.contains?(intersection_point) && other.contains?(intersection_point)
165
+ return [intersection_point]
166
+ end
167
+
168
+ return []
169
+ else
170
+ # we're skew
171
+ return []
172
+ end
173
+ end
174
+
175
+ if other.respond_to?(:intersection)
176
+ return other.intersection(self)
177
+ end
178
+
179
+ raise TypeError, "Intersection between LinearEntity and #{ other.class } is not defined"
180
+ end
181
+
182
+ # Create a new Line parallel to this linear entity which passes
183
+ # through the point `p`
184
+ #
185
+ # Parameters:
186
+ # Point
187
+ #
188
+ # Returns:
189
+ # Line
190
+ #
191
+ def parallel_line(point)
192
+ raise TypeError, 'Must pass only Point.' unless point.is_a?(Point)
193
+ Line.new(point, point + self.direction.to_point)
194
+ end
195
+
196
+ # Create a new Line perpendicular to this linear entity which passes
197
+ # through the point `point`.
198
+ #
199
+ # Parameters:
200
+ # Point
201
+ #
202
+ # Returns:
203
+ # Line
204
+ #
205
+ def perpendicular_line(point)
206
+ raise TypeError, 'Must pass only Point.' unless point.is_a?(Point)
207
+
208
+ # any two lines in R^2 intersect, so blindly making
209
+ # a line through p in an orthogonal direction will work
210
+ Line.new(point, point + self.direction.orthogonal_direction.to_point)
211
+ end
212
+
213
+ # Create a perpendicular line segment from `point` to this line.
214
+ # The enpoints of the segment are `point` and the closest point in
215
+ # the line containing self. (If self is not a line, the point might
216
+ # not be in self.)
217
+ #
218
+ # Parameters:
219
+ # Point
220
+ #
221
+ # Returns:
222
+ # Segment or Point (if `point` is on this linear entity.)
223
+ #
224
+ def perpendicular_segment(point)
225
+ raise TypeError, 'Must pass only Point.' unless point.is_a?(Point)
226
+
227
+ return point if self.contains?(point)
228
+
229
+ l = self.perpendicular_line(point)
230
+ p = Line.new(self.p1, self.p2).intersection(l).first
231
+
232
+ Segment.new(point, p)
233
+ end
234
+
235
+ # The slope of this linear entity, or infinity if vertical.
236
+ #
237
+ # Returns:
238
+ # number or BigDecimal('Infinity')
239
+ #
240
+ def slope
241
+ return @slope if defined?(@slope)
242
+
243
+ dx = p1.x - p2.x
244
+ dy = p1.y - p2.y
245
+
246
+ if dy == 0
247
+ @slope = 0.0
248
+ elsif dx == 0
249
+ @slope = BigDecimal('Infinity')
250
+ else
251
+ @slope = dy / dx
252
+ end
253
+
254
+ @slope
255
+ end
256
+
257
+ # Test whether the point `other` lies in the positive span of `self`.
258
+ # A point x is 'in front' of a point y if x.dot(y) >= 0.
259
+ #
260
+ # Return
261
+ # -1 if `other` is behind `self.p1`,
262
+ # 0 if `other` is `self.p1`
263
+ # 1 if `other` is in front of `self.p1`.
264
+ #
265
+ def span_test(other)
266
+ raise TypeError, 'Must pass only Point.' unless other.is_a?(Point)
267
+ return 0 if self.p1 == other
268
+
269
+ rel_pos = other - self.p1
270
+ return 1 if self.direction.to_point.dot(rel_pos) > 0
271
+
272
+ return -1
273
+ end
274
+
275
+ # Project a point onto this linear entity.
276
+ #
277
+ # Parameters:
278
+ # Point
279
+ #
280
+ # Returns:
281
+ # Point
282
+ #
283
+ def projection_point(p)
284
+ Point.project(p - p1, self.direction.to_point) + p1
285
+ end
286
+
287
+ private
288
+
289
+ def intersect_parallel_rays(ray1, ray2)
290
+ if ray1.direction.dot(ray2.direction) > 0
291
+ # rays point in the same direction
292
+ # so return the one that is "in front"
293
+ return [ray2] if ray1.span_test(ray2.p1) >= 0
294
+ return [ray1]
295
+ end
296
+
297
+ # rays point in opposite directions
298
+ st = ray1.span_test(ray2.p1)
299
+ return [] if st < 0
300
+ return [ray2.p1] if st == 0
301
+
302
+ [Segment.new(ray1.p1, ray2.p1)]
303
+ end
304
+
305
+ def intersect_parallel_ray_and_segment(ray, seg)
306
+ st1 = ray.span_test(seg.p1)
307
+ st2 = ray.span_test(seg.p2)
308
+
309
+ if st1 < 0 && st2 < 0
310
+ return []
311
+ elsif st1 >= 0 && st2 >= 0
312
+ return [seg]
313
+ elsif st1 >= 0 # st2 < 0
314
+ return [ray.p1] if ray.p1 == seg.p1
315
+ return [Segment.new(ray.p1, seg.p1)]
316
+ elsif st2 >= 0 # st1 < 0
317
+ return [ray.p1] if ray.p1 == seg.p2
318
+ return [Segment.new(ray.p1, seg.p2)]
319
+ end
320
+ end
321
+
322
+ def intersect_parallel_segments(seg1, seg2)
323
+ return [seg2] if seg1.contains?(seg2)
324
+ return [seg1] if seg2.contains?(seg1)
325
+
326
+ # direct the segments so they're oriented the same way
327
+ if seg1.direction.dot(seg2.direction) < 0
328
+ seg2 = Segment.new(seg2.p2, seg2.p1)
329
+ end
330
+
331
+ # order the segments so seg1 is "behind" seg2
332
+ if seg1.span_test(seg2.p1) < 0
333
+ seg1, seg2 = seg2, seg1
334
+ end
335
+
336
+ return [] if seg2.span_test(seg1.p2) < 0
337
+ return [seg2.p1] if seg2.p1 == seg1.p2
338
+
339
+ [Segment.new(seg2.p1, seg1.p2)]
340
+ end
341
+
342
+ def check_input_points!
343
+ @p1 = Point.new(p1[0], p1[1]) if p1.is_a?(Array)
344
+ raise TypeError, "Point should be array or instance of class Point." unless p1.is_a?(Point)
345
+
346
+ @p2 = Point.new(p2[0], p2[1]) if p2.is_a?(Array)
347
+ raise TypeError, "Point should be array or instance of class Point." unless p2.is_a?(Point)
348
+ end
349
+
350
+ def validate!
351
+ raise ArgumentError, "Segment requires two unique Points." if p1 == p2
352
+ end
353
+ end
354
+ end
355
+ end