geometry 6 → 6.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ed229b8577bfe815ff25060d851e6f27515cf7a7
4
+ data.tar.gz: 9ed183df010e991b72c216639acda3f2f68884d5
5
+ SHA512:
6
+ metadata.gz: ea42b04b4aa1bc9f89f5d64cc2e96ff61a066bff34108d6b534e492da37dd69a7aa8163f5388bb46ae350c726926074d9a201480d642c376518bf3efc63c631b
7
+ data.tar.gz: bafb8c4996b9017d28f43d7b56a0b1e253d738f20ade966aa196efca041513f2ebd502be560926a3404b9314c04ce3f13e4c8c382e2a1dd06d797062ada2c7ac
@@ -14,7 +14,7 @@ that don't work in higher dimensions and I'll do my best to fix them.
14
14
  License
15
15
  -------
16
16
 
17
- Copyright 2012-2013 Brandon Fosdick <bfoz@bfoz.net> and released under the BSD license.
17
+ Copyright 2012-2014 Brandon Fosdick <bfoz@bfoz.net> and released under the BSD license.
18
18
 
19
19
  Primitives
20
20
  ----------
@@ -45,6 +45,11 @@ Examples
45
45
  point.x
46
46
  point.y
47
47
  point[2] # Same as point.z
48
+
49
+ # Zero
50
+ PointZero.new # A Point full of zeros of unspecified length
51
+ Point.zero # Another way to do the same thing
52
+ Point.zero(3) # => Point[0,0,0]
48
53
  ```
49
54
 
50
55
  ### Line
data/Rakefile CHANGED
@@ -5,7 +5,6 @@ task :default => :test
5
5
 
6
6
  Rake::TestTask.new do |t|
7
7
  t.libs.push "lib"
8
- # t.test_files = FileList['test/**/rectangle.rb']
9
8
  t.test_files = FileList['test/**/*.rb']
10
9
  t.verbose = true
11
10
  end
@@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__)
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "geometry"
6
- s.version = '6'
6
+ s.version = '6.1'
7
7
  s.authors = ["Brandon Fosdick"]
8
8
  s.email = ["bfoz@bfoz.net"]
9
9
  s.homepage = "http://github.com/bfoz/geometry"
@@ -87,6 +87,9 @@ Supports two-point, slope-intercept, and point-slope initializer forms
87
87
 
88
88
  # @private
89
89
  class PointSlopeLine < Line
90
+ # @return [Number] the slope of the {Line}
91
+ attr_reader :slope
92
+
90
93
  def initialize(point, slope)
91
94
  @point = Point[point]
92
95
  @slope = slope
@@ -98,6 +101,9 @@ Supports two-point, slope-intercept, and point-slope initializer forms
98
101
 
99
102
  # @private
100
103
  class SlopeInterceptLine < Line
104
+ # @return [Number] the slope of the {Line}
105
+ attr_reader :slope
106
+
101
107
  def initialize(slope, intercept)
102
108
  @slope = slope
103
109
  @intercept = intercept
@@ -118,9 +124,6 @@ Supports two-point, slope-intercept, and point-slope initializer forms
118
124
  vertical? ? nil : @intercept
119
125
  end
120
126
  end
121
- def slope
122
- @slope
123
- end
124
127
 
125
128
  def to_s
126
129
  'Line(' + @slope.to_s + ',' + @intercept.to_s + ')'
@@ -138,6 +141,14 @@ Supports two-point, slope-intercept, and point-slope initializer forms
138
141
  'Line(' + @first.inspect + ', ' + @last.inspect + ')'
139
142
  end
140
143
  alias :to_s :inspect
144
+
145
+ # @group Accessors
146
+ # !@attribute [r[ slope
147
+ # @return [Number] the slope of the {Line}
148
+ def slope
149
+ (last.y - first.y)/(last.x - first.x)
150
+ end
151
+ # @endgroup
141
152
  end
142
153
  end
143
154
 
@@ -35,10 +35,16 @@ geometry class (x, y, z).
35
35
  super *array
36
36
  end
37
37
 
38
- # Creates and returns a new {PointZero} instance
38
+ # Creates and returns a new {PointZero} instance. Or, a {Point} full of zeros if the size argument is given.
39
+ # @param size [Number] the size of the new {Point} full of zeros
39
40
  # @return [PointZero] A new {PointZero} instance
40
- def self.zero
41
- PointZero.new
41
+ def self.zero(size=nil)
42
+ size ? Point[Array.new(size, 0)] : PointZero.new
43
+ end
44
+
45
+ # Return a copy of the {Point}
46
+ def clone
47
+ Point[@elements.clone]
42
48
  end
43
49
 
44
50
  # Allow comparison with an Array, otherwise do the normal thing
@@ -128,8 +134,8 @@ geometry class (x, y, z).
128
134
  case other
129
135
  when Numeric
130
136
  Point[@elements.map {|e| e + other}]
131
- when PointZero
132
- self
137
+ when PointZero, NilClass
138
+ self.dup
133
139
  else
134
140
  raise OperationNotDefined, "#{other.class} must respond to :size and :[]" unless other.respond_to?(:size) && other.respond_to?(:[])
135
141
  raise DimensionMismatch, "Can't add #{other} to #{self}" if size != other.size
@@ -141,8 +147,8 @@ geometry class (x, y, z).
141
147
  case other
142
148
  when Numeric
143
149
  Point[@elements.map {|e| e - other}]
144
- when PointZero
145
- self
150
+ when PointZero, NilClass
151
+ self.dup
146
152
  else
147
153
  raise OperationNotDefined, "#{other.class} must respond to :size and :[]" unless other.respond_to?(:size) && other.respond_to?(:[])
148
154
  raise DimensionMismatch, "Can't subtract #{other} from #{self}" if size != other.size
@@ -5,7 +5,8 @@ module Geometry
5
5
  An object repesenting a {Point} at the origin in N-dimensional space
6
6
 
7
7
  A {PointZero} object is a {Point} that will always compare equal to zero and unequal to
8
- everything else, regardless of size.
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}.
9
10
  =end
10
11
  class PointZero
11
12
  def eql?(other)
@@ -42,19 +43,19 @@ everything else, regardless of size.
42
43
  end
43
44
 
44
45
  # @attribute [r] x
45
- # @return [Numeric] X-component
46
+ # @return [Numeric] X-component
46
47
  def x
47
48
  0
48
49
  end
49
50
 
50
51
  # @attribute [r] y
51
- # @return [Numeric] Y-component
52
+ # @return [Numeric] Y-component
52
53
  def y
53
54
  0
54
55
  end
55
56
 
56
57
  # @attribute [r] z
57
- # @return [Numeric] Z-component
58
+ # @return [Numeric] Z-component
58
59
  def z
59
60
  0
60
61
  end
@@ -29,9 +29,13 @@ but there's currently nothing that enforces simplicity.
29
29
  # @return [Polygon]
30
30
  def initialize(*args)
31
31
  super
32
+ close! # A Polygon is always closed
33
+ end
32
34
 
33
- # Close the polygon if needed
34
- @edges.push Edge.new(@edges.last.last, @edges.first.first) unless @edges.empty? || (@edges.last.last == @edges.first.first)
35
+ # This method returns the receiver because a {Polygon} is always closed
36
+ # @return [Polygon] the receiver
37
+ def close
38
+ close!
35
39
  end
36
40
 
37
41
  # Check the orientation of the {Polygon}
@@ -45,6 +49,19 @@ but there's currently nothing that enforces simplicity.
45
49
  self.class.new *(self.vertices.reverse)
46
50
  end
47
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
+
48
65
  # @group Boolean operators
49
66
 
50
67
  # Test a {Point} for inclusion in the receiver using a simplified winding number algorithm
@@ -205,16 +222,16 @@ but there's currently nothing that enforces simplicity.
205
222
  # @param [Number] distance The distance to offset by
206
223
  # @return [Polygon] A new {Polygon} outset by the given distance
207
224
  def outset(distance)
208
- bisectors = offset_bisectors(distance)
209
- offsets = (bisectors.each_cons(2).to_a << [bisectors.last, bisectors.first])
225
+ bisector_edges = outset_bisectors(distance)
226
+ bisector_pairs = bisector_edges.push(bisector_edges.first).each_cons(2)
210
227
 
211
228
  # Create the offset edges and then wrap them in Hashes so the edges
212
229
  # can be altered while walking the array
213
- active_edges = edges.zip(offsets).map do |e,offset|
214
- offset = Edge.new(e.first+offset.first.vector, e.last+offset.last.vector)
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)
215
232
 
216
233
  # Skip zero-length edges
217
- {:edge => (offset.first == offset.last) ? nil : offset}
234
+ {:edge => (offset_edge.first == offset_edge.last) ? nil : offset_edge}
218
235
  end
219
236
 
220
237
  # Walk the array and handle any intersections
@@ -239,6 +256,7 @@ but there's currently nothing that enforces simplicity.
239
256
  # Handle the collinear case
240
257
  active_edges[i][:edge] = Edge.new(e1.first, e2.last)
241
258
  active_edges[j].delete(:edge)
259
+ wrap_around_is_shortest = false
242
260
  end
243
261
 
244
262
  # Delete everything between e1 and e2
@@ -263,29 +281,17 @@ but there's currently nothing that enforces simplicity.
263
281
  Polygon.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten)
264
282
  end
265
283
 
266
- # Vertex bisectors suitable for offsetting
284
+ # Vertex bisectors suitable for outsetting
267
285
  # @param [Number] length The distance to offset by
268
286
  # @return [Array<Edge>] {Edge}s representing the bisectors
269
- def offset_bisectors(length)
270
- vectors = edges.map {|e| e.direction }
271
- winding = 0
272
- sums = vectors.unshift(vectors.last).each_cons(2).map do |v1,v2|
273
- k = v1[0]*v2[1] - v1[1]*v2[0] # z-component of v1 x v2
274
- winding += k
275
- if v1 == v2 # collinear, same direction?
276
- Vector[-v1[1], v1[0]]
277
- elsif 0 == k # collinear, reverse direction
278
- nil
279
- else
280
- by = (v2[1] - v1[1])/k
281
- v = (0 == v1[1]) ? v2 : v1
282
- Vector[(v[0]*by - 1)/v[1], by]
283
- end
284
- end
287
+ def outset_bisectors(length)
288
+ vertices.zip(spokes).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil}
289
+ end
285
290
 
286
- # Check the polygon's orientation. If clockwise, negate length as a hack for injecting a -1 into the final result
287
- length = -length if winding >= 0
288
- vertices.zip(sums).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil}
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
289
295
  end
290
296
 
291
297
  private
@@ -16,7 +16,7 @@ also like a {Path} in that it isn't necessarily closed.
16
16
  attr_reader :edges, :vertices
17
17
 
18
18
  # Construct a new Polyline from Points and/or Edges
19
- # The constructor will try to convert all of its arguments into {Point}s and
19
+ # @note The constructor will try to convert all of its arguments into {Point}s and
20
20
  # {Edge}s. Then successive {Point}s will be collpased into {Edge}s. Successive
21
21
  # {Edge}s that share a common vertex will be added to the new {Polyline}. If
22
22
  # there's a gap between {Edge}s it will be automatically filled with a new
@@ -94,21 +94,94 @@ also like a {Path} in that it isn't necessarily closed.
94
94
  end
95
95
  alias :== :eql?
96
96
 
97
- # Offset the receiver by the specified distance. A positive distance
98
- # will offset to the left, and a negative distance to the right.
97
+ # Clone the receiver, close it, then return it
98
+ # @return [Polyline] the closed clone of the receiver
99
+ def close
100
+ clone.close!
101
+ end
102
+
103
+ # Close the receiver and return it
104
+ # @return [Polyline] the receiver after closing
105
+ def close!
106
+ push_edge Edge.new(@edges.last.last, @edges.first.first) unless @edges.empty? || closed?
107
+ self
108
+ end
109
+
110
+ # Check to see if the {Polyline} is closed (ie. is it a {Polygon}?)
111
+ # @return [Bool] true if the {Polyline} is closed (the first vertex is equal to the last vertex)
112
+ def closed?
113
+ @edges.last.last == @edges.first.first
114
+ end
115
+
116
+ # Clone the receiver, reverse it, then return it
117
+ # @return [Polyline] the reversed clone
118
+ def reverse
119
+ self.class.new *(edges.reverse.map! {|edge| edge.reverse! })
120
+ end
121
+
122
+ # Reverse the receiver and return it
123
+ # @return [Polyline] the reversed receiver
124
+ def reverse!
125
+ vertices.reverse!
126
+ edges.reverse!.map! {|edge| edge.reverse! }
127
+ self
128
+ end
129
+
130
+ # @group Bisectors
131
+
132
+ # Generate the angle bisector unit vectors for each vertex
133
+ # @note If the {Polyline} isn't closed (the normal case), then the first and
134
+ # last vertices will be given bisectors that are perpendicular to themselves.
135
+ # @return [Array<Vector>] the unit {Vector}s representing the angle bisector of each vertex
136
+ def bisectors
137
+ # Multiplying each bisector by the sign of k flips any bisectors that aren't pointing towards the interior of the angle
138
+ bisector_map {|b, k| k <=> 0 }
139
+ end
140
+
141
+ # Generate left-side angle bisector unit vectors for each vertex
142
+ # @note This is similar to the #bisector method, but generates vectors that always point to the left side of the {Polyline} instead of towards the inside of each corner
143
+ # @return [Array<Vector>] the unit {Vector}s representing the left-side angle bisector of each vertex
144
+ def left_bisectors
145
+ bisector_map
146
+ end
147
+
148
+ # Generate right-side angle bisector unit vectors for each vertex
149
+ # @note This is similar to the #bisector method, but generates vectors that always point to the right side of the {Polyline} instead of towards the inside of each corner
150
+ # @return [Array<Vector>] the unit {Vector}s representing the ride-side angle bisector of each vertex
151
+ def right_bisectors
152
+ bisector_map {|b, k| -1 }
153
+ end
154
+
155
+ # Generate the spokes for each vertex. A spoke is the same as a bisector, but in the oppostire direction (bisectors point towards the inside of each corner; spokes point towards the outside)
156
+ # @note If the {Polyline} isn't closed (the normal case), then the first and
157
+ # last vertices will be given bisectors that are perpendicular to themselves.
158
+ # @return [Array<Vector>] the unit {Vector}s representing the spoke of each vertex
159
+ def spokes
160
+ # Multiplying each bisector by the negated sign of k flips any bisectors that aren't pointing towards the exterior of the angle
161
+ bisector_map {|b, k| 0 <=> k }
162
+ end
163
+
164
+ # @endgroup Bisectors
165
+
166
+ # Offset the receiver by the specified distance
167
+ # @note A positive distance will offset to the left, and a negative distance to the right.
99
168
  # @param [Number] distance The distance to offset by
100
- # @return [Polygon] A new {Polygon} outset by the given distance
169
+ # @return [Polyline] A new {Polyline} outset by the given distance
101
170
  def offset(distance)
102
- bisectors = offset_bisectors(distance)
103
- offsets = bisectors.each_cons(2).to_a
171
+ bisector_pairs = if closed?
172
+ bisector_edges = offset_bisectors(distance)
173
+ bisector_edges.push(bisector_edges.first).each_cons(2)
174
+ else
175
+ offset_bisectors(distance).each_cons(2)
176
+ end
104
177
 
105
178
  # Create the offset edges and then wrap them in Hashes so the edges
106
179
  # can be altered while walking the array
107
- active_edges = edges.zip(offsets).map do |e,offset|
108
- offset = Edge.new(e.first+offset.first.vector, e.last+offset.last.vector)
180
+ active_edges = edges.zip(bisector_pairs).map do |e,offset|
181
+ offset_edge = Edge.new(e.first+offset.first.vector, e.last+offset.last.vector)
109
182
 
110
183
  # Skip zero-length edges
111
- {:edge => (offset.first == offset.last) ? nil : offset}
184
+ {:edge => (offset_edge.first == offset_edge.last) ? nil : offset_edge}
112
185
  end
113
186
 
114
187
  # Walk the array and handle any intersections
@@ -143,36 +216,69 @@ also like a {Path} in that it isn't necessarily closed.
143
216
 
144
217
  # Rightset the receiver by the specified distance
145
218
  # @param [Number] distance The distance to offset by
146
- # @return [Polygon] A new {Polygon} rightset by the given distance
219
+ # @return [Polyline] A new {Polyline} rightset by the given distance
147
220
  def rightset(distance)
148
221
  offset(-distance)
149
222
  end
150
223
 
151
224
  private
152
225
 
153
- # @group Helpers for offset()
154
-
155
- # Vertex bisectors suitable for offsetting
156
- # @param [Number] length The distance to offset by
157
- # @return [Array<Edge>] {Edge}s representing the bisectors
158
- def offset_bisectors(length)
159
- vectors = edges.map {|e| e.direction }
226
+ # Generate bisectors and k values with an optional mapping block
227
+ # @note If the {Polyline} isn't closed (the normal case), then the first and
228
+ # last vertices will be given bisectors that are perpendicular to themselves.
229
+ # @return [Array<Vector>] the unit {Vector}s representing the angle bisector of each vertex
230
+ def bisector_map
160
231
  winding = 0
161
- sums = vectors.unshift(vectors.first).push(vectors.last).each_cons(2).map do |v1,v2|
232
+ tangent_loop.each_cons(2).map do |v1,v2|
162
233
  k = v1[0]*v2[1] - v1[1]*v2[0] # z-component of v1 x v2
163
234
  winding += k
164
235
  if v1 == v2 # collinear, same direction?
165
- Vector[-v1[1], v1[0]]
236
+ bisector = Vector[-v1[1], v1[0]]
237
+ block_given? ? (bisector * yield(bisector, 1)) : bisector
166
238
  elsif 0 == k # collinear, reverse direction
167
239
  nil
168
240
  else
169
- by = (v2[1] - v1[1])/k
170
- v = (0 == v1[1]) ? v2 : v1
171
- Vector[(v[0]*by - 1)/v[1], by]
241
+ bisector_y = (v2[1] - v1[1])/k
242
+
243
+ # If v1 or v2 happens to be horizontal, then the other one must be used when calculating
244
+ # the x-component of the bisector (to avoid a divide by zero). But, comparing floats
245
+ # with zero is problematic, so use the one with the largest y-component instead checking
246
+ # for a y-component equal to zero.
247
+ v = (v2[1].abs > v1[1].abs) ? v2 : v1
248
+
249
+ bisector = Vector[(v[0]*bisector_y - 1)/v[1], bisector_y]
250
+ block_given? ? (bisector * yield(bisector, k)) : bisector
172
251
  end
173
252
  end
253
+ end
174
254
 
175
- vertices.zip(sums).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil}
255
+ # @group Helpers for offset()
256
+
257
+ # Vertex bisectors suitable for offsetting
258
+ # @param [Number] length The distance to offset by. Positive generates left offset bisectors, negative generates right offset bisectors
259
+ # @return [Array<Edge>] {Edge}s representing the bisectors
260
+ def offset_bisectors(length)
261
+ vertices.zip(left_bisectors).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil}
262
+ end
263
+
264
+ # Generate the tangents and fake a circular buffer while accounting for closedness
265
+ # @return [Array<Vector>] the tangents
266
+ def tangent_loop
267
+ edges.map {|e| e.direction }.tap do |tangents|
268
+ # Generating a bisector for each vertex requires an edge on both sides of each vertex.
269
+ # Obviously, the first and last vertices each have only a single adjacent edge, unless the
270
+ # Polyline happens to be closed (like a Polygon). When not closed, duplicate the
271
+ # first and last direction vectors to fake the adjacent edges. This causes the first and last
272
+ # edges to have bisectors that are perpendicular to themselves.
273
+ if closed?
274
+ # Prepend the last direction vector so that the last edge can be used to find the bisector for the first vertex
275
+ tangents.unshift tangents.last
276
+ else
277
+ # Duplicate the first and last direction vectors to compensate for not having edges adjacent to the first and last vertices
278
+ tangents.unshift(tangents.first)
279
+ tangents.push(tangents.last)
280
+ end
281
+ end
176
282
  end
177
283
 
178
284
  # Find the next edge that intersects with e, starting at index i