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
data/lib/geometry/rectangle.rb
CHANGED
@@ -78,6 +78,8 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
|
|
78
78
|
SizedRectangle.new(height: options[:height], width: options[:width])
|
79
79
|
elsif (2==args.count) and (args.all? {|a| a.is_a?(Array) || a.is_a?(Point) })
|
80
80
|
original_new(*args)
|
81
|
+
elsif options.empty?
|
82
|
+
raise ArgumentError, "#{self} arguments must be named, not: #{args}"
|
81
83
|
else
|
82
84
|
raise ArgumentError, "Bad Rectangle arguments: #{args}, #{options}"
|
83
85
|
end
|
@@ -171,6 +173,39 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
|
|
171
173
|
max.x - min.x
|
172
174
|
end
|
173
175
|
# @endgroup
|
176
|
+
|
177
|
+
# Create a new {Rectangle} from the receiver that's inset by the given amount
|
178
|
+
# @overload inset(x, y)
|
179
|
+
# @overload inset(top, left, bottom, right)
|
180
|
+
# @overload inset(x, y)
|
181
|
+
# @option options [Number] :x Inset from the left and right sides
|
182
|
+
# @option options [Number] :y Inset from the top and bottom
|
183
|
+
# @overload inset(top, left, bottom, right)
|
184
|
+
# @option options [Number] :bottom The inset from the bottom of the {Rectangle}
|
185
|
+
# @option options [Number] :left The inset from the left side of the {Rectangle}
|
186
|
+
# @option options [Number] :right The inset from the right side of the {Rectangle}
|
187
|
+
# @option options [Number] :top The inset from the top of the {Rectangle}
|
188
|
+
def inset(*args)
|
189
|
+
options, args = args.partition {|a| a.is_a? Hash}
|
190
|
+
options = options.reduce({}, :merge)
|
191
|
+
raise ArumentError, "Can't specify both arguments and options" if !args.empty? && !options.empty?
|
192
|
+
|
193
|
+
if 1 == args.size
|
194
|
+
distance = args.shift
|
195
|
+
Rectangle.new from:(min + distance), to:(max - distance)
|
196
|
+
elsif 2 == args.size
|
197
|
+
distance = Point[*args]
|
198
|
+
Rectangle.new from:(min + distance), to:(max - distance)
|
199
|
+
elsif 4 == args.size
|
200
|
+
top, left, bottom, right = *args
|
201
|
+
Rectangle.new from:(min + Point[left, bottom]), to:(max - Point[right, top])
|
202
|
+
elsif options[:x] && options[:y]
|
203
|
+
distance = Point[options[:x], options[:y]]
|
204
|
+
Rectangle.new from:(min + distance), to:(max - distance)
|
205
|
+
elsif options[:top] && options[:left] && options[:bottom] && options[:right]
|
206
|
+
Rectangle.new from:(min + Point[options[:left], options[:bottom]]), to:(max - Point[options[:right], options[:top]])
|
207
|
+
end
|
208
|
+
end
|
174
209
|
end
|
175
210
|
|
176
211
|
class CenteredRectangle < Rectangle
|
@@ -27,18 +27,18 @@ A {RegularPolygon} is a lot like a {Polygon}, but more regular.
|
|
27
27
|
# @overload new(sides, center, radius)
|
28
28
|
# Construct a {RegularPolygon} using a center point and radius
|
29
29
|
# @option options [Number] :sides The number of edges
|
30
|
-
# @option options [Point] :center The center point of the {RegularPolygon}
|
30
|
+
# @option options [Point] :center (PointZero) The center point of the {RegularPolygon}
|
31
31
|
# @option options [Number] :radius The radius of the {RegularPolygon}
|
32
32
|
# @overload new(sides, center, diameter)
|
33
33
|
# Construct a {RegularPolygon} using a center point and diameter
|
34
34
|
# @option options [Number] :sides The number of edges
|
35
|
-
# @option options [Point] :center The center point of the {RegularPolygon}
|
35
|
+
# @option options [Point] :center (PointZero) The center point of the {RegularPolygon}
|
36
36
|
# @option options [Number] :diameter The diameter of the {RegularPolygon}
|
37
37
|
def self.new(options={}, &block)
|
38
38
|
raise ArgumentError, "RegularPolygon requires an edge count" unless options[:sides]
|
39
39
|
|
40
40
|
center = options[:center]
|
41
|
-
center = center ? Point[center] :
|
41
|
+
center = center ? Point[center] : Point.zero
|
42
42
|
|
43
43
|
if options.has_key?(:radius)
|
44
44
|
self.allocate.tap {|polygon| polygon.send :initialize, options[:sides], center, options[:radius], &block }
|
@@ -66,11 +66,43 @@ A {RegularPolygon} is a lot like a {Polygon}, but more regular.
|
|
66
66
|
alias :== :eql?
|
67
67
|
|
68
68
|
# @!group Accessors
|
69
|
+
# @return [Rectangle] The smallest axis-aligned {Rectangle} that bounds the receiver
|
70
|
+
def bounds
|
71
|
+
return Rectangle.new(self.min, self.max)
|
72
|
+
end
|
73
|
+
|
69
74
|
# @!attribute [r] diameter
|
70
75
|
# @return [Numeric] The diameter of the {RegularPolygon}
|
71
76
|
def diameter
|
72
77
|
@radius*2
|
73
78
|
end
|
79
|
+
|
80
|
+
# !@attribute [r] edges
|
81
|
+
def edges
|
82
|
+
points = self.vertices
|
83
|
+
points.each_cons(2).map {|p1,p2| Edge.new(p1,p2) } + [Edge.new(points.last, points.first)]
|
84
|
+
end
|
85
|
+
|
86
|
+
# !@attribute [r] vertices
|
87
|
+
# @return [Array]
|
88
|
+
def vertices
|
89
|
+
(0...2*Math::PI).step(2*Math::PI/edge_count).map {|angle| center + Point[Math::cos(angle), Math::sin(angle)]*radius }
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Point] The upper right corner of the bounding {Rectangle}
|
93
|
+
def max
|
94
|
+
@center+Point[radius, radius]
|
95
|
+
end
|
96
|
+
|
97
|
+
# @return [Point] The lower left corner of the bounding {Rectangle}
|
98
|
+
def min
|
99
|
+
@center-Point[radius, radius]
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [Array<Point>] The lower left and upper right corners of the bounding {Rectangle}
|
103
|
+
def minmax
|
104
|
+
[self.min, self.max]
|
105
|
+
end
|
74
106
|
# @!endgroup
|
75
107
|
end
|
76
108
|
|
data/lib/geometry/rotation.rb
CHANGED
@@ -1,16 +1,39 @@
|
|
1
1
|
require 'matrix'
|
2
2
|
|
3
|
+
require_relative 'cluster_factory'
|
4
|
+
require_relative 'point'
|
5
|
+
|
3
6
|
module Geometry
|
4
7
|
=begin
|
5
8
|
A generalized representation of a rotation transformation.
|
9
|
+
|
10
|
+
== Usage
|
11
|
+
Rotation.new angle:45*Math.PI/180 # Rotate 45 degrees counterclockwise
|
12
|
+
Rotation.new x:[0,1] # Rotate 90 degrees counterclockwise
|
6
13
|
=end
|
7
14
|
class Rotation
|
8
|
-
|
9
|
-
|
15
|
+
include ClusterFactory
|
16
|
+
|
17
|
+
# @return [Integer] dimensions
|
10
18
|
attr_reader :dimensions
|
11
19
|
attr_reader :x, :y, :z
|
12
20
|
|
21
|
+
# @overload new(angle)
|
22
|
+
# Create a planar {Rotation} with an angle
|
23
|
+
def self.new(*args)
|
24
|
+
options = args.select {|a| a.is_a? Hash}.reduce({}, :merge)
|
25
|
+
|
26
|
+
if options.has_key? :angle
|
27
|
+
RotationAngle.new options[:angle]
|
28
|
+
elsif options.has_key?(:x) && [:x, :y, :z].one? {|k| options.has_key? k }
|
29
|
+
RotationAngle.new x:options[:x]
|
30
|
+
else
|
31
|
+
self.allocate.tap {|rotation| rotation.send :initialize, *args }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
13
35
|
# @overload initialize(options={})
|
36
|
+
# @option options [Radians] :angle Planar rotation angle
|
14
37
|
# @option options [Integer] :dimensions Dimensionality of the rotation
|
15
38
|
# @option options [Vector] :x X-axis
|
16
39
|
# @option options [Vector] :y Y-axis
|
@@ -53,8 +76,10 @@ A generalized representation of a rotation transformation.
|
|
53
76
|
end
|
54
77
|
|
55
78
|
# @attribute [r] matrix
|
56
|
-
# @return [Matrix]
|
79
|
+
# @return [Matrix] the transformation {Matrix} representing the {Rotation}
|
57
80
|
def matrix
|
81
|
+
return nil unless [@x, @y, @z].compact.size >= 2
|
82
|
+
|
58
83
|
# Force all axes to be Vectors
|
59
84
|
x,y,z = [@x, @y, @z].map {|a| a.is_a?(Array) ? Vector[*a] : a}
|
60
85
|
|
@@ -74,5 +99,92 @@ A generalized representation of a rotation transformation.
|
|
74
99
|
|
75
100
|
Matrix[*rows]
|
76
101
|
end
|
102
|
+
|
103
|
+
|
104
|
+
# Transform and return a new {Point}
|
105
|
+
# @param [Point] point the {Point} to rotate into the parent coordinate frame
|
106
|
+
# @return [Point] the rotated {Point}
|
107
|
+
def transform(point)
|
108
|
+
m = matrix
|
109
|
+
m ? Point[m * Point[point]] : point
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class RotationAngle < Rotation
|
114
|
+
# @return [Radians] the planar rotation angle
|
115
|
+
attr_accessor :angle
|
116
|
+
|
117
|
+
# @option options [Radians] :angle the rotation angle from the parent coordinate frame
|
118
|
+
# @option options [Point] :x the X-axis expressed in the parent coordinate frame
|
119
|
+
def initialize(*args)
|
120
|
+
options, args = args.partition {|a| a.is_a? Hash}
|
121
|
+
options = options.reduce({}, :merge)
|
122
|
+
|
123
|
+
angle = options[:angle] || args[0]
|
124
|
+
|
125
|
+
if angle
|
126
|
+
@angle = angle
|
127
|
+
elsif options.has_key? :x
|
128
|
+
@angle = Math.atan2(*options[:x].to_a.reverse)
|
129
|
+
else
|
130
|
+
@angle = 0
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def eql?(other)
|
135
|
+
case other
|
136
|
+
when RotationAngle then angle.eql? other.angle
|
137
|
+
else
|
138
|
+
false
|
139
|
+
end
|
140
|
+
end
|
141
|
+
alias :== :eql?
|
142
|
+
|
143
|
+
# @group Accessors
|
144
|
+
# !@attribute [r] matrix
|
145
|
+
# @return [Matrix] the transformation {Matrix} representing the {Rotation}
|
146
|
+
def matrix
|
147
|
+
return nil unless angle
|
148
|
+
|
149
|
+
c, s = Math.cos(angle), Math.sin(angle)
|
150
|
+
Matrix[[c, -s], [s, c]]
|
151
|
+
end
|
152
|
+
|
153
|
+
# !@attribute [r] x
|
154
|
+
# @return [Point] the X-axis expressed in the parent coordinate frame
|
155
|
+
def x
|
156
|
+
Point[Math.cos(angle), Math.sin(angle)]
|
157
|
+
end
|
158
|
+
|
159
|
+
# !@attribute [r] y
|
160
|
+
# @return [Point] the Y-axis expressed in the parent coordinate frame
|
161
|
+
def y
|
162
|
+
Point[-Math.sin(angle), Math.cos(angle)]
|
163
|
+
end
|
164
|
+
# @endgroup
|
165
|
+
|
166
|
+
# @group Composition
|
167
|
+
def -@
|
168
|
+
RotationAngle.new(-angle)
|
169
|
+
end
|
170
|
+
|
171
|
+
def +(other)
|
172
|
+
case other
|
173
|
+
when RotationAngle
|
174
|
+
RotationAngle.new(angle + other.angle)
|
175
|
+
else
|
176
|
+
raise TypeError, "Can't compose a #{self.class} with a #{other.class}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def -(other)
|
181
|
+
case other
|
182
|
+
when RotationAngle
|
183
|
+
RotationAngle.new(angle - other.angle)
|
184
|
+
else
|
185
|
+
raise TypeError, "Can't subtract #{other.class} from #{self.class}"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
# @endgroup
|
77
189
|
end
|
78
190
|
end
|
data/lib/geometry/size_zero.rb
CHANGED
@@ -5,7 +5,8 @@ module Geometry
|
|
5
5
|
An object repesenting a zero {Size} in N-dimensional space
|
6
6
|
|
7
7
|
A {SizeZero} object is a {Size} that will always compare equal to zero and unequal to
|
8
|
-
everything else, regardless of dimensionality.
|
8
|
+
everything else, regardless of dimensionality. 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 SizeZero
|
11
12
|
def eql?(other)
|
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'geometry/point'
|
2
2
|
require 'geometry/rotation'
|
3
3
|
|
4
|
+
require_relative 'transformation/composition'
|
5
|
+
|
4
6
|
module Geometry
|
5
7
|
=begin
|
6
|
-
{Transformation} represents a relationship between two coordinate frames
|
8
|
+
{Transformation} represents a relationship between two coordinate frames.
|
7
9
|
|
8
10
|
To create a pure translation relationship:
|
9
11
|
|
@@ -38,6 +40,7 @@ system's X-axis:
|
|
38
40
|
# @option options [Point] :origin Same as :translate
|
39
41
|
# @option options [Point] :move Same as :translate
|
40
42
|
# @option options [Point] :translate Linear displacement
|
43
|
+
# @option options [Angle] :angle Rotation angle (assumes planar geometry)
|
41
44
|
# @option options [Rotation] :rotate Rotation
|
42
45
|
# @option options [Vector] :scale Scaling
|
43
46
|
# @option options [Vector] :x X-axis
|
@@ -50,7 +53,8 @@ system's X-axis:
|
|
50
53
|
|
51
54
|
@dimensions = options[:dimensions] || nil
|
52
55
|
|
53
|
-
|
56
|
+
rotation_options = options.select {|key, value| [:angle, :x, :y, :z].include? key }
|
57
|
+
@rotation = options[:rotate] || rotate || ((rotation_options.size > 0) ? Geometry::Rotation.new(rotation_options) : nil)
|
54
58
|
@scale = options[:scale] || scale
|
55
59
|
|
56
60
|
case options.count {|k,v| [:move, :origin, :translate].include? k }
|
@@ -62,6 +66,7 @@ system's X-axis:
|
|
62
66
|
raise ArgumentError, "Too many translation parameters in #{options}"
|
63
67
|
end
|
64
68
|
|
69
|
+
raise ArgumentError, "Bad translation" if @translation.is_a? Hash
|
65
70
|
@translation = Point[*@translation]
|
66
71
|
if @translation
|
67
72
|
@translation = nil if @translation.all? {|v| v == 0}
|
@@ -77,42 +82,90 @@ system's X-axis:
|
|
77
82
|
end
|
78
83
|
end
|
79
84
|
|
85
|
+
def initialize_clone(source)
|
86
|
+
super
|
87
|
+
@rotation = @rotation.clone if @rotation
|
88
|
+
@scale = @scale.clone if @scale
|
89
|
+
@translation = @translation.clone if @translation
|
90
|
+
end
|
91
|
+
|
92
|
+
# !@attribute [r] has_rotation?
|
93
|
+
# @return [Bool] true if the transformation has any rotation components
|
94
|
+
def has_rotation?
|
95
|
+
!!@rotation
|
96
|
+
end
|
97
|
+
|
80
98
|
# Returns true if the {Transformation} is the identity transformation
|
81
99
|
def identity?
|
82
|
-
@rotation
|
100
|
+
!(@rotation || @scale || @translation)
|
83
101
|
end
|
84
102
|
|
85
103
|
def eql?(other)
|
86
|
-
|
104
|
+
return false unless other
|
105
|
+
return true if !self.dimensions && !other.dimensions && !self.rotation && !other.rotation && !self.translation && !other.translation && !self.scale && !other.scale
|
106
|
+
return false if !(self.dimensions && other.dimensions) && !(self.rotation && other.rotation) && !(self.translation && other.translation) && !(self.scale && other.scale)
|
107
|
+
|
108
|
+
((self.dimensions && other.dimensions && self.dimensions.eql?(other.dimensions)) || !(self.dimensions && other.dimensions)) &&
|
109
|
+
((self.rotation && other.rotation && self.rotation.eql?(other.rotation)) || !(self.rotation && other.rotation)) &&
|
110
|
+
((self.scale && other.scale && self.scale.eql?(other.scale)) || !(self.scale && other.rotation)) &&
|
111
|
+
((self.translation && other.translation && self.translation.eql?(other.translation)) || !(self.scale && other.rotation))
|
87
112
|
end
|
88
113
|
alias :== :eql?
|
89
114
|
|
90
115
|
# Compose the current {Transformation} with another one
|
91
116
|
def +(other)
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
117
|
+
return self.clone unless other
|
118
|
+
|
119
|
+
case other
|
120
|
+
when Array, Vector
|
121
|
+
if @translation
|
122
|
+
Transformation.new(@translation+other, @rotation, @scale)
|
123
|
+
else
|
124
|
+
Transformation.new(other, @rotation, @scale)
|
125
|
+
end
|
126
|
+
when Composition
|
127
|
+
Composition.new(self, *other.transformations)
|
128
|
+
when Transformation
|
129
|
+
if @rotation || other.rotation
|
130
|
+
Composition.new(self, other)
|
131
|
+
else
|
132
|
+
translation = @translation ? (@translation + other.translation) : other.translation
|
133
|
+
Transformation.new(translation, @rotation, @scale)
|
134
|
+
end
|
98
135
|
end
|
99
136
|
end
|
100
137
|
|
101
138
|
def -(other)
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
139
|
+
return self.clone unless other
|
140
|
+
|
141
|
+
case other
|
142
|
+
when Array, Vector
|
143
|
+
if @translation
|
144
|
+
Transformation.new(@translation-other, @rotation, @scale)
|
145
|
+
else
|
146
|
+
Transformation.new(other.map {|e| -e}, @rotation, @scale)
|
147
|
+
end
|
148
|
+
when Transformation
|
149
|
+
if @rotation
|
150
|
+
rotation = other.rotation ? (@rotation - other.rotation) : @rotation
|
151
|
+
elsif other.rotation
|
152
|
+
rotation = -other.rotation
|
153
|
+
else
|
154
|
+
rotation = nil
|
155
|
+
end
|
156
|
+
|
157
|
+
translation = @translation ? (@translation - other.translation) : -other.translation
|
158
|
+
|
159
|
+
Transformation.new(translation, rotation, @scale)
|
108
160
|
end
|
109
161
|
end
|
110
162
|
|
111
|
-
# Transform and return a new {Point}
|
112
|
-
# @param [Point] point
|
163
|
+
# Transform and return a new {Point}. Rotation is applied before translation.
|
164
|
+
# @param [Point] point the {Point} to transform into the parent coordinate frame
|
113
165
|
# @return [Point] The transformed {Point}
|
114
166
|
def transform(point)
|
115
|
-
@
|
167
|
+
point = @rotation.transform(point) if @rotation
|
168
|
+
@translation ? (@translation + point) : point
|
116
169
|
end
|
117
170
|
end
|
118
171
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Geometry
|
2
|
+
class Transformation
|
3
|
+
class Composition
|
4
|
+
attr_reader :transformations
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
raise TypeError unless args.all? {|a| a.is_a? Transformation }
|
8
|
+
@transformations = *args
|
9
|
+
end
|
10
|
+
|
11
|
+
def +(other)
|
12
|
+
case other
|
13
|
+
when Transformation
|
14
|
+
Composition.new(*transformations, other)
|
15
|
+
when Composition
|
16
|
+
Composition.new(*transformations, *other.transformations)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# @group Accessors
|
21
|
+
# !@attribute [r] has_rotation?
|
22
|
+
# @return [Bool] true if the transformation has any rotation components
|
23
|
+
def has_rotation?
|
24
|
+
transformations.any? {|t| t.is_a?(Rotation) || t.has_rotation? }
|
25
|
+
end
|
26
|
+
|
27
|
+
# !@attribute [r] size
|
28
|
+
# @return [Number] the number of composed {Transformation}s
|
29
|
+
def size
|
30
|
+
transformations.size
|
31
|
+
end
|
32
|
+
# @endgroup
|
33
|
+
|
34
|
+
def transform(point)
|
35
|
+
transformations.reverse.reduce(point) {|point, transformation| transformation.transform(point) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|