geometry 6 → 6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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] : nil
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
 
@@ -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
- # @attribute [r] dimensions
9
- # @return [Integer]
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
@@ -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
- @rotation = options[:rotate] || rotate || Geometry::Rotation.new(options)
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.identity? && !(@scale || @translation)
100
+ !(@rotation || @scale || @translation)
83
101
  end
84
102
 
85
103
  def eql?(other)
86
- (self.rotation.eql? other.rotation) && (self.scale.eql? other.scale) && (self.translation.eql? other.translation)
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
- if other.is_a?(Array) or other.is_a?(Vector)
93
- if @translation
94
- Transformation.new(@translation+other, @rotation, @scale)
95
- else
96
- Transformation.new(other, @rotation, @scale)
97
- end
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
- if other.is_a?(Array) or other.is_a?(Vector)
103
- if @translation
104
- Transformation.new(@translation-other, @rotation, @scale)
105
- else
106
- Transformation.new(other.map {|e| -e}, @rotation, @scale)
107
- end
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 The {Point} to transform
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
- @translation + point
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