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