geometry-in-ruby 0.0.1

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.
Files changed (51) hide show
  1. data/.gitignore +6 -0
  2. data/Gemfile +7 -0
  3. data/LICENSE +21 -0
  4. data/README.markdown +105 -0
  5. data/Rakefile +24 -0
  6. data/geometry-in-ruby.gemspec +23 -0
  7. data/lib/geometry/arc.rb +94 -0
  8. data/lib/geometry/circle.rb +122 -0
  9. data/lib/geometry/cluster_factory.rb +15 -0
  10. data/lib/geometry/edge.rb +140 -0
  11. data/lib/geometry/line.rb +154 -0
  12. data/lib/geometry/obround.rb +238 -0
  13. data/lib/geometry/path.rb +67 -0
  14. data/lib/geometry/point.rb +163 -0
  15. data/lib/geometry/point_zero.rb +107 -0
  16. data/lib/geometry/polygon.rb +368 -0
  17. data/lib/geometry/polyline.rb +318 -0
  18. data/lib/geometry/rectangle.rb +378 -0
  19. data/lib/geometry/regular_polygon.rb +136 -0
  20. data/lib/geometry/rotation.rb +190 -0
  21. data/lib/geometry/size.rb +75 -0
  22. data/lib/geometry/size_zero.rb +70 -0
  23. data/lib/geometry/square.rb +113 -0
  24. data/lib/geometry/text.rb +24 -0
  25. data/lib/geometry/transformation/composition.rb +39 -0
  26. data/lib/geometry/transformation.rb +171 -0
  27. data/lib/geometry/triangle.rb +78 -0
  28. data/lib/geometry/vector.rb +34 -0
  29. data/lib/geometry.rb +22 -0
  30. data/test/geometry/arc.rb +25 -0
  31. data/test/geometry/circle.rb +112 -0
  32. data/test/geometry/edge.rb +132 -0
  33. data/test/geometry/line.rb +132 -0
  34. data/test/geometry/obround.rb +25 -0
  35. data/test/geometry/path.rb +66 -0
  36. data/test/geometry/point.rb +258 -0
  37. data/test/geometry/point_zero.rb +177 -0
  38. data/test/geometry/polygon.rb +214 -0
  39. data/test/geometry/polyline.rb +266 -0
  40. data/test/geometry/rectangle.rb +154 -0
  41. data/test/geometry/regular_polygon.rb +120 -0
  42. data/test/geometry/rotation.rb +108 -0
  43. data/test/geometry/size.rb +97 -0
  44. data/test/geometry/size_zero.rb +153 -0
  45. data/test/geometry/square.rb +66 -0
  46. data/test/geometry/transformation/composition.rb +49 -0
  47. data/test/geometry/transformation.rb +169 -0
  48. data/test/geometry/triangle.rb +32 -0
  49. data/test/geometry/vector.rb +41 -0
  50. data/test/geometry.rb +5 -0
  51. metadata +117 -0
@@ -0,0 +1,107 @@
1
+ require_relative 'point'
2
+
3
+ module Geometry
4
+ =begin rdoc
5
+ An object repesenting a {Point} at the origin in N-dimensional space
6
+
7
+ A {PointZero} object is a {Point} that will always compare equal to zero and unequal to
8
+ everything else, regardless of size. You can think of it as an application of the
9
+ {http://en.wikipedia.org/wiki/Null_Object_pattern Null Object Pattern}.
10
+ =end
11
+ class PointZero
12
+ def eql?(other)
13
+ if other.respond_to? :all?
14
+ other.all? {|e| e.eql? 0}
15
+ else
16
+ other == 0
17
+ end
18
+ end
19
+ alias == eql?
20
+
21
+ def coerce(other)
22
+ if other.is_a? Numeric
23
+ [other, 0]
24
+ elsif other.is_a? Array
25
+ [other, Array.new(other.size,0)]
26
+ elsif other.is_a? Vector
27
+ [other, Vector[*Array.new(other.size,0)]]
28
+ else
29
+ [Point[other], Point[Array.new(other.size,0)]]
30
+ end
31
+ end
32
+
33
+ # This is a hack to get Array#== to work properly. It works on ruby 2.0 and 1.9.3.
34
+ def to_ary
35
+ []
36
+ end
37
+
38
+ # @group Accessors
39
+ # @param [Integer] i Index into the {Point}'s elements
40
+ # @return [Numeric] Element i (starting at 0)
41
+ def [](i)
42
+ 0
43
+ end
44
+
45
+ # @attribute [r] x
46
+ # @return [Numeric] X-component
47
+ def x
48
+ 0
49
+ end
50
+
51
+ # @attribute [r] y
52
+ # @return [Numeric] Y-component
53
+ def y
54
+ 0
55
+ end
56
+
57
+ # @attribute [r] z
58
+ # @return [Numeric] Z-component
59
+ def z
60
+ 0
61
+ end
62
+ # @endgroup
63
+
64
+ # @group Arithmetic
65
+
66
+ # @group Unary operators
67
+ def +@
68
+ self
69
+ end
70
+
71
+ def -@
72
+ self
73
+ end
74
+ # @endgroup
75
+
76
+ def +(other)
77
+ case other
78
+ when Array, Numeric then other
79
+ else
80
+ Point[other]
81
+ end
82
+ end
83
+
84
+ def -(other)
85
+ if other.is_a? Size
86
+ -Point[other]
87
+ elsif other.respond_to? :-@
88
+ -other
89
+ elsif other.respond_to? :map
90
+ other.map {|a| -a }
91
+ end
92
+ end
93
+
94
+ def *(other)
95
+ self
96
+ end
97
+
98
+ def /(other)
99
+ raise OperationNotDefined unless other.is_a? Numeric
100
+ raise ZeroDivisionError if 0 == other
101
+ self
102
+ end
103
+ # @endgroup
104
+
105
+ end
106
+ end
107
+
@@ -0,0 +1,368 @@
1
+ require_relative 'edge'
2
+ require_relative 'polyline'
3
+
4
+ module Geometry
5
+
6
+ =begin rdoc
7
+ A {Polygon} is a closed path comprised entirely of lines so straight they don't even curve.
8
+
9
+ {http://en.wikipedia.org/wiki/Polygon}
10
+
11
+ The {Polygon} class is generally intended to represent {http://en.wikipedia.org/wiki/Simple_polygon Simple polygons},
12
+ but there's currently nothing that enforces simplicity.
13
+
14
+ == Usage
15
+
16
+ =end
17
+
18
+ class Polygon < Polyline
19
+
20
+ # Construct a new Polygon from Points and/or Edges
21
+ # The constructor will try to convert all of its arguments into Points and
22
+ # Edges. Then successive Points will be collpased into Edges. Successive
23
+ # Edges that share a common vertex will be added to the new Polygon. If
24
+ # there's a gap between Edges it will be automatically filled with a new
25
+ # Edge. The resulting Polygon will then be closed if it isn't already.
26
+ # @overload initialize(Edge, Edge, ...)
27
+ # @return [Polygon]
28
+ # @overload initialize(Point, Point, ...)
29
+ # @return [Polygon]
30
+ def initialize(*args)
31
+ super
32
+ close! # A Polygon is always closed
33
+ end
34
+
35
+ # This method returns the receiver because a {Polygon} is always closed
36
+ # @return [Polygon] the receiver
37
+ def close
38
+ close!
39
+ end
40
+
41
+ # Check the orientation of the {Polygon}
42
+ # @return [Boolean] True if the {Polygon} is clockwise, otherwise false
43
+ def clockwise?
44
+ edges.map {|e| (e.last.x - e.first.x) * (e.last.y + e.first.y)}.reduce(:+) >= 0
45
+ end
46
+
47
+ # @return [Polygon] A new {Polygon} with orientation that's the opposite of the receiver
48
+ def reverse
49
+ self.class.new *(self.vertices.reverse)
50
+ end
51
+
52
+ # Reverse the receiver and return it
53
+ # @return [Polygon] the reversed receiver
54
+ def reverse!
55
+ super
56
+
57
+ # Simply reversing the vertex array causes the reversed polygon to
58
+ # start at what had been the last vertex, instead of starting at
59
+ # the same vertex and just going the other direction.
60
+ vertices.unshift vertices.pop
61
+
62
+ self
63
+ end
64
+
65
+ # @group Boolean operators
66
+
67
+ # Test a {Point} for inclusion in the receiver using a simplified winding number algorithm
68
+ # @param [Point] point The {Point} to test
69
+ # @return [Number] 1 if the {Point} is inside the {Polygon}, -1 if it's outside, and 0 if it's on an {Edge}
70
+ def <=>(point)
71
+ sum = edges.reduce(0) do |sum, e|
72
+ direction = e.last.y <=> e.first.y
73
+ # Ignore edges that don't cross the point's x coordinate
74
+ next sum unless ((point.y <=> e.last.y) + (point.y <=> e.first.y)).abs <= 1
75
+
76
+ if 0 == direction # Special case horizontal edges
77
+ return 0 if ((point.x <=> e.last.x) + (point.x <=> e.first.x)).abs <= 1
78
+ next sum # Doesn't intersect
79
+ else
80
+ is_left = e <=> point
81
+ return 0 if 0 == is_left
82
+ next sum unless is_left
83
+ sum += 0 <=> (direction + is_left)
84
+ end
85
+ end
86
+ (0 == sum) ? -1 : 1
87
+ end
88
+
89
+ # Create a new {Polygon} that's the union of the receiver and a passed {Polygon}
90
+ # This is a simplified implementation of the alogrithm outlined in the
91
+ # paper {http://gvu.gatech.edu/people/official/jarek/graphics/papers/04PolygonBooleansMargalit.pdf An algorithm for computing the union, intersection or difference of two polygons}.
92
+ # In particular, this method assumes the receiver and passed {Polygon}s are "island" type and that the desired output is "regular", as those terms are described in the paper.
93
+ # @param [Polygon] other The {Polygon} to union with the receiver
94
+ # @return [Polygon] The union of the receiver and the passed {Polygon}
95
+ def union(other)
96
+ # Table 1: Both polygons are islands and the operation is union, so both must have the same orientation
97
+ # Reverse the other polygon if the orientations are different
98
+ other = other.reverse if self.clockwise? != other.clockwise?
99
+
100
+ # Receiver's vertex ring
101
+ ringA = VertexRing.new
102
+ self.vertices.each {|v| ringA.push v, (other <=> v)}
103
+
104
+ # The other vertex ring
105
+ ringB = VertexRing.new
106
+ other.vertices.each {|v| ringB.push v, (self <=> v)}
107
+
108
+ # Find intersections
109
+ offsetA = 0
110
+ edgesB = other.edges.dup
111
+ self.edges.each_with_index do |a, indexA|
112
+ offsetB = 0
113
+ ringB.edges_with_index do |b, indexB|
114
+ intersection = a.intersection(b)
115
+ if intersection === true
116
+ if (a.first == b.first) and (a.last == b.last) # Equal edges
117
+ elsif (a.first == b.last) and (a.last == b.first) # Ignore equal but opposite edges
118
+ else
119
+ if a.vector.normalize == b.vector.normalize # Same direction?
120
+ offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, b.first)
121
+ offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, a.last)
122
+ else # Opposite direction
123
+ offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, b.last)
124
+ offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, a.first)
125
+ end
126
+ end
127
+ elsif intersection.is_a?(Point)
128
+ offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, intersection)
129
+ offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, intersection)
130
+ end
131
+ end
132
+ end
133
+
134
+ # Table 2: Both polygons are islands and the operation is union, so select outside from both polygons
135
+ edgeFragments = []
136
+ [[ringA, other], [ringB, self]].each do |ring, other_polygon|
137
+ ring.edges do |v1,v2|
138
+ if (v1[:type] == -1) or (v2[:type] == -1)
139
+ edgeFragments.push :first => v1[:vertex], :last => v2[:vertex]
140
+ elsif (v1[:type] == 0) and (v2[:type] == 0)
141
+ if (other_polygon <=> Point[(v1[:vertex] + v2[:vertex])/2]) <= 0
142
+ edgeFragments.push :first => v1[:vertex], :last => v2[:vertex]
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ # Delete any duplicated edges. Array#uniq doesn't do the right thing, so using inject instead.
149
+ edgeFragments = edgeFragments.inject([]) {|result,h| result << h unless result.include?(h); result}
150
+
151
+ # Delete any equal-and-opposite edges
152
+ edgeFragments = edgeFragments.reject {|f| edgeFragments.find {|f2| (f[:first] == f2[:last]) and (f[:last] == f2[:first])} }
153
+
154
+ # Construct the output polygons
155
+ output = edgeFragments.reduce([Array.new]) do |output, fragment|
156
+ next output if fragment.empty?
157
+ polygon = output.last
158
+ polygon.push fragment[:first], fragment[:last] if polygon.empty?
159
+ while 1 do
160
+ adjacent_fragment = edgeFragments.find {|f| fragment[:last] == f[:first]}
161
+ break unless adjacent_fragment
162
+
163
+ polygon.push adjacent_fragment[:first], adjacent_fragment[:last]
164
+ fragment = adjacent_fragment.dup
165
+ adjacent_fragment.clear
166
+
167
+ break if polygon.first == polygon.last # closed?
168
+ end
169
+ output << Array.new
170
+ end
171
+
172
+ # If everything worked properly there should be only one output Polygon
173
+ output.reject! {|a| a.empty?}
174
+ output = Polygon.new *(output[0])
175
+
176
+ # Table 4: Both input polygons are "island" type and the operation
177
+ # is union, so the output polygon's orientation should be the same
178
+ # as the input polygon's orientation
179
+ (self.clockwise? != output.clockwise?) ? output.reverse : output
180
+ end
181
+ alias :+ :union
182
+
183
+ # @endgroup
184
+
185
+ # @group Convex Hull
186
+
187
+ # Returns the convex hull of the {Polygon}
188
+ # @return [Polygon] A convex {Polygon}, or the original {Polygon} if it's already convex
189
+ def convex
190
+ wrap
191
+ end
192
+
193
+ # Returns the convex hull using the {http://en.wikipedia.org/wiki/Gift_wrapping_algorithm Gift Wrapping algorithm}
194
+ # This implementation was cobbled together from many sources, but mostly from this implementation of the {http://butunclebob.com/ArticleS.UncleBob.ConvexHullTiming Jarvis March}
195
+ # @return [Polygon]
196
+ def wrap
197
+ # Start with a Point that's guaranteed to be on the hull
198
+ leftmost_point = vertices.min_by {|v| v.x}
199
+ current_point = vertices.select {|v| v.x == leftmost_point.x}.min_by {|v| v.y}
200
+
201
+ current_angle = 0.0
202
+ hull_points = [current_point]
203
+ while true
204
+ min_angle = 4.0
205
+ min_point = nil
206
+ vertices.each do |v1|
207
+ next if current_point.equal? v1
208
+ angle = pseudo_angle_for_edge(current_point, v1)
209
+ min_point, min_angle = v1, angle if (angle >= current_angle) && (angle <= min_angle)
210
+ end
211
+ current_angle = min_angle
212
+ current_point = min_point
213
+ break if current_point == hull_points.first
214
+ hull_points << min_point
215
+ end
216
+ Polygon.new *hull_points
217
+ end
218
+
219
+ # @endgroup
220
+
221
+ # Outset the receiver by the specified distance
222
+ # @param [Number] distance The distance to offset by
223
+ # @return [Polygon] A new {Polygon} outset by the given distance
224
+ def outset(distance)
225
+ bisector_edges = outset_bisectors(distance)
226
+ bisector_pairs = bisector_edges.push(bisector_edges.first).each_cons(2)
227
+
228
+ # Create the offset edges and then wrap them in Hashes so the edges
229
+ # can be altered while walking the array
230
+ active_edges = edges.zip(bisector_pairs).map do |e,offset|
231
+ offset_edge = Edge.new(e.first+offset.first.vector, e.last+offset.last.vector)
232
+
233
+ # Skip zero-length edges
234
+ {:edge => (offset_edge.first == offset_edge.last) ? nil : offset_edge}
235
+ end
236
+
237
+ # Walk the array and handle any intersections
238
+ active_edges.each_with_index do |e, i|
239
+ e1 = e[:edge]
240
+ next unless e1 # Ignore deleted edges
241
+
242
+ intersection, j = find_last_intersection(active_edges, i, e1)
243
+ if intersection
244
+ e2 = active_edges[j][:edge]
245
+ wrap_around_is_shortest = ((i + active_edges.count - j) < (j-i))
246
+
247
+ if intersection.is_a? Point
248
+ if wrap_around_is_shortest
249
+ active_edges[i][:edge] = Edge.new(intersection, e1.last)
250
+ active_edges[j][:edge] = Edge.new(e2.first, intersection)
251
+ else
252
+ active_edges[i][:edge] = Edge.new(e1.first, intersection)
253
+ active_edges[j][:edge] = Edge.new(intersection, e2.last)
254
+ end
255
+ else
256
+ # Handle the collinear case
257
+ active_edges[i][:edge] = Edge.new(e1.first, e2.last)
258
+ active_edges[j].delete(:edge)
259
+ wrap_around_is_shortest = false
260
+ end
261
+
262
+ # Delete everything between e1 and e2
263
+ if wrap_around_is_shortest # Choose the shortest path
264
+ for k in 0...i do
265
+ active_edges[k].delete(:edge)
266
+ end
267
+ for k in j...active_edges.count do
268
+ next if k==j # Exclude e2
269
+ active_edges[k].delete(:edge)
270
+ end
271
+ else
272
+ for k in i...j do
273
+ next if k==i # Exclude e1 and e2
274
+ active_edges[k].delete(:edge)
275
+ end
276
+ end
277
+
278
+ redo # Recheck the modified edges
279
+ end
280
+ end
281
+ Polygon.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten)
282
+ end
283
+
284
+ # Vertex bisectors suitable for outsetting
285
+ # @param [Number] length The distance to offset by
286
+ # @return [Array<Edge>] {Edge}s representing the bisectors
287
+ def outset_bisectors(length)
288
+ vertices.zip(spokes).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil}
289
+ end
290
+
291
+ # Generate the unit-length spokes for each vertex
292
+ # @return [Array<Vector>] the unit {Vector}s representing the spoke of each vertex
293
+ def spokes
294
+ clockwise? ? left_bisectors : right_bisectors
295
+ end
296
+
297
+ private
298
+
299
+ # Return a number that increases with the slope of the {Edge}
300
+ # @return [Number] A number in the range [0,4)
301
+ def pseudo_angle_for_edge(point0, point1)
302
+ delta = Point[point1.x.to_f, point1.y.to_f] - Point[point0.x.to_f, point0.y.to_f]
303
+ if delta.x >= 0
304
+ if delta.y >= 0
305
+ quadrant_one_psuedo_angle(delta.x, delta.y)
306
+ else
307
+ 1 + quadrant_one_psuedo_angle(delta.y.abs, delta.x)
308
+ end
309
+ else
310
+ if delta.y >= 0
311
+ 3 + quadrant_one_psuedo_angle(delta.y, delta.x.abs)
312
+ else
313
+ 2 + quadrant_one_psuedo_angle(delta.x.abs, delta.y.abs)
314
+ end
315
+ end
316
+ end
317
+
318
+ def quadrant_one_psuedo_angle(dx, dy)
319
+ dx / (dx + dy)
320
+ end
321
+ end
322
+
323
+ private
324
+
325
+ class VertexRing
326
+ attr_reader :vertices
327
+
328
+ def initialize
329
+ @vertices = []
330
+ end
331
+
332
+ # @param [Integer] index The index to insert the new {Point} before
333
+ # @param [Point] point The {Point} to insert
334
+ # @param [Integer] type The vertex type: 1 is inside, 0 is boundary, -1 is outside
335
+ def insert(index, point, type)
336
+ if v = @vertices.find {|v| v[:vertex] == point }
337
+ v[:type] = type
338
+ false
339
+ else
340
+ @vertices.insert(index, {:vertex => point, :type => type})
341
+ true
342
+ end
343
+ end
344
+
345
+ # Insert a boundary vertex
346
+ # @param [Integer] index The index to insert the new {Point} before
347
+ # @param [Point] point The {Point} to insert
348
+ def insert_boundary(index, point)
349
+ self.insert(index, point, 0)
350
+ end
351
+
352
+ # @param [Point] point The {Point} to push
353
+ # @param [Integer] type The vertex type: 1 is inside, 0 is boundary, -1 is outside
354
+ def push(point, type)
355
+ @vertices << {:vertex => point, :type => type}
356
+ end
357
+
358
+ # Enumerate the pairs of vertices corresponding to each edge
359
+ def edges
360
+ (@vertices + [@vertices.first]).each_cons(2) {|v1,v2| yield v1, v2}
361
+ end
362
+
363
+ def edges_with_index
364
+ index = 0
365
+ (@vertices + [@vertices.first]).each_cons(2) {|v1,v2| yield(Edge.new(v1[:vertex], v2[:vertex]), index); index += 1}
366
+ end
367
+ end
368
+ end