geometry 6 → 6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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