geometry 6 → 6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.markdown +6 -1
- data/Rakefile +0 -1
- data/geometry.gemspec +1 -1
- data/lib/geometry/line.rb +14 -3
- data/lib/geometry/point.rb +13 -7
- data/lib/geometry/point_zero.rb +5 -4
- data/lib/geometry/polygon.rb +33 -27
- data/lib/geometry/polyline.rb +129 -23
- data/lib/geometry/rectangle.rb +35 -0
- data/lib/geometry/regular_polygon.rb +35 -3
- data/lib/geometry/rotation.rb +115 -3
- data/lib/geometry/size_zero.rb +2 -1
- data/lib/geometry/transformation.rb +72 -19
- data/lib/geometry/transformation/composition.rb +39 -0
- data/test/geometry/edge.rb +14 -7
- data/test/geometry/line.rb +24 -0
- data/test/geometry/point.rb +55 -14
- data/test/geometry/polygon.rb +28 -6
- data/test/geometry/polyline.rb +176 -0
- data/test/geometry/rectangle.rb +18 -1
- data/test/geometry/regular_polygon.rb +38 -2
- data/test/geometry/rotation.rb +60 -0
- data/test/geometry/transformation.rb +76 -10
- data/test/geometry/transformation/composition.rb +49 -0
- metadata +10 -15
checksums.yaml
ADDED
@@ -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
|
data/README.markdown
CHANGED
@@ -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-
|
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
data/geometry.gemspec
CHANGED
data/lib/geometry/line.rb
CHANGED
@@ -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
|
|
data/lib/geometry/point.rb
CHANGED
@@ -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
|
data/lib/geometry/point_zero.rb
CHANGED
@@ -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
|
-
#
|
46
|
+
# @return [Numeric] X-component
|
46
47
|
def x
|
47
48
|
0
|
48
49
|
end
|
49
50
|
|
50
51
|
# @attribute [r] y
|
51
|
-
#
|
52
|
+
# @return [Numeric] Y-component
|
52
53
|
def y
|
53
54
|
0
|
54
55
|
end
|
55
56
|
|
56
57
|
# @attribute [r] z
|
57
|
-
#
|
58
|
+
# @return [Numeric] Z-component
|
58
59
|
def z
|
59
60
|
0
|
60
61
|
end
|
data/lib/geometry/polygon.rb
CHANGED
@@ -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
|
-
|
34
|
-
|
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
|
-
|
209
|
-
|
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(
|
214
|
-
|
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 => (
|
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
|
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
|
270
|
-
|
271
|
-
|
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
|
-
|
287
|
-
|
288
|
-
|
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
|
data/lib/geometry/polyline.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
#
|
98
|
-
#
|
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 [
|
169
|
+
# @return [Polyline] A new {Polyline} outset by the given distance
|
101
170
|
def offset(distance)
|
102
|
-
|
103
|
-
|
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(
|
108
|
-
|
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 => (
|
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 [
|
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
|
-
#
|
154
|
-
|
155
|
-
#
|
156
|
-
# @
|
157
|
-
|
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
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
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
|