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.
- 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
|