aurora-geometry 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/Gemfile +7 -0
  4. data/LICENSE +21 -0
  5. data/README.markdown +105 -0
  6. data/Rakefile +24 -0
  7. data/aurora-geometry.gemspec +23 -0
  8. data/lib/geometry.rb +22 -0
  9. data/lib/geometry/arc.rb +94 -0
  10. data/lib/geometry/circle.rb +122 -0
  11. data/lib/geometry/cluster_factory.rb +15 -0
  12. data/lib/geometry/edge.rb +140 -0
  13. data/lib/geometry/line.rb +154 -0
  14. data/lib/geometry/obround.rb +238 -0
  15. data/lib/geometry/path.rb +67 -0
  16. data/lib/geometry/point.rb +163 -0
  17. data/lib/geometry/point_zero.rb +107 -0
  18. data/lib/geometry/polygon.rb +368 -0
  19. data/lib/geometry/polyline.rb +318 -0
  20. data/lib/geometry/rectangle.rb +378 -0
  21. data/lib/geometry/regular_polygon.rb +136 -0
  22. data/lib/geometry/rotation.rb +190 -0
  23. data/lib/geometry/size.rb +75 -0
  24. data/lib/geometry/size_zero.rb +70 -0
  25. data/lib/geometry/square.rb +113 -0
  26. data/lib/geometry/text.rb +24 -0
  27. data/lib/geometry/transformation.rb +171 -0
  28. data/lib/geometry/transformation/composition.rb +39 -0
  29. data/lib/geometry/triangle.rb +78 -0
  30. data/lib/geometry/vector.rb +34 -0
  31. data/test/geometry.rb +5 -0
  32. data/test/geometry/arc.rb +25 -0
  33. data/test/geometry/circle.rb +112 -0
  34. data/test/geometry/edge.rb +132 -0
  35. data/test/geometry/line.rb +132 -0
  36. data/test/geometry/obround.rb +25 -0
  37. data/test/geometry/path.rb +66 -0
  38. data/test/geometry/point.rb +258 -0
  39. data/test/geometry/point_zero.rb +177 -0
  40. data/test/geometry/polygon.rb +214 -0
  41. data/test/geometry/polyline.rb +266 -0
  42. data/test/geometry/rectangle.rb +154 -0
  43. data/test/geometry/regular_polygon.rb +120 -0
  44. data/test/geometry/rotation.rb +108 -0
  45. data/test/geometry/size.rb +97 -0
  46. data/test/geometry/size_zero.rb +153 -0
  47. data/test/geometry/square.rb +66 -0
  48. data/test/geometry/transformation.rb +169 -0
  49. data/test/geometry/transformation/composition.rb +49 -0
  50. data/test/geometry/triangle.rb +32 -0
  51. data/test/geometry/vector.rb +41 -0
  52. metadata +115 -0
@@ -0,0 +1,136 @@
1
+ require_relative 'cluster_factory'
2
+ require_relative 'polygon'
3
+
4
+ module Geometry
5
+ =begin rdoc
6
+ A {RegularPolygon} is a lot like a {Polygon}, but more regular.
7
+
8
+ {http://en.wikipedia.org/wiki/Regular_polygon}
9
+
10
+ == Usage
11
+ polygon = Geometry::RegularPolygon.new sides:4, center:[1,2], radius:3
12
+ polygon = Geometry::RegularPolygon.new sides:6, center:[1,2], diameter:6
13
+ =end
14
+
15
+ class RegularPolygon < Polygon
16
+ include ClusterFactory
17
+
18
+ # @return [Point] The {RegularPolygon}'s center point
19
+ attr_reader :center
20
+
21
+ # @return [Number] The {RegularPolygon}'s number of sides
22
+ attr_reader :edge_count
23
+
24
+ # @return [Number] The {RegularPolygon}'s radius
25
+ attr_reader :radius
26
+
27
+ # @overload new(sides, center, radius)
28
+ # Construct a {RegularPolygon} using a center point and radius
29
+ # @option options [Number] :sides The number of edges
30
+ # @option options [Point] :center (PointZero) The center point of the {RegularPolygon}
31
+ # @option options [Number] :radius The radius of the {RegularPolygon}
32
+ # @overload new(sides, center, diameter)
33
+ # Construct a {RegularPolygon} using a center point and diameter
34
+ # @option options [Number] :sides The number of edges
35
+ # @option options [Point] :center (PointZero) The center point of the {RegularPolygon}
36
+ # @option options [Number] :diameter The diameter of the {RegularPolygon}
37
+ def self.new(options={}, &block)
38
+ raise ArgumentError, "RegularPolygon requires an edge count" unless options[:sides]
39
+
40
+ center = options[:center]
41
+ center = center ? Point[center] : Point.zero
42
+
43
+ if options.has_key?(:radius)
44
+ self.allocate.tap {|polygon| polygon.send :initialize, options[:sides], center, options[:radius], &block }
45
+ elsif options.has_key?(:diameter)
46
+ DiameterRegularPolygon.new options[:sides], center, options[:diameter], &block
47
+ else
48
+ raise ArgumentError, "RegularPolygon.new requires a radius or a diameter"
49
+ end
50
+ end
51
+
52
+ # Construct a new {RegularPolygon} from a centerpoint and radius
53
+ # @param [Number] edge_count The number of edges
54
+ # @param [Point] center The center point of the {Circle}
55
+ # @param [Number] radius The radius of the {Circle}
56
+ # @return [RegularPolygon] A new {RegularPolygon} object
57
+ def initialize(edge_count, center, radius)
58
+ @center = Point[center]
59
+ @edge_count = edge_count
60
+ @radius = radius
61
+ end
62
+
63
+ def eql?(other)
64
+ (self.center == other.center) && (self.edge_count == other.edge_count) && (self.radius == other.radius)
65
+ end
66
+ alias :== :eql?
67
+
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
+
74
+ # @!attribute [r] diameter
75
+ # @return [Numeric] The diameter of the {RegularPolygon}
76
+ def diameter
77
+ @radius*2
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
106
+ # @!endgroup
107
+ end
108
+
109
+ class DiameterRegularPolygon < RegularPolygon
110
+ # @return [Number] The {RegularPolygon}'s diameter
111
+ attr_reader :diameter
112
+
113
+ # Construct a new {RegularPolygon} from a centerpoint and a diameter
114
+ # @param [Number] edge_count The number of edges
115
+ # @param [Point] center The center point of the {RegularPolygon}
116
+ # @param [Number] diameter The radius of the {RegularPolygon}
117
+ # @return [RegularPolygon] A new {RegularPolygon} object
118
+ def initialize(edge_count, center, diameter)
119
+ @center = center ? Point[center] : nil
120
+ @edge_count = edge_count
121
+ @diameter = diameter
122
+ end
123
+
124
+ def eql?(other)
125
+ (self.center == other.center) && (self.edge_count == other.edge_count) && (self.diameter == other.diameter)
126
+ end
127
+ alias :== :eql?
128
+
129
+ # @!group Accessors
130
+ # @return [Number] The {RegularPolygon}'s radius
131
+ def radius
132
+ @diameter/2
133
+ end
134
+ # @!endgroup
135
+ end
136
+ end
@@ -0,0 +1,190 @@
1
+ require 'matrix'
2
+
3
+ require_relative 'cluster_factory'
4
+ require_relative 'point'
5
+
6
+ module Geometry
7
+ =begin
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
13
+ =end
14
+ class Rotation
15
+ include ClusterFactory
16
+
17
+ # @return [Integer] dimensions
18
+ attr_reader :dimensions
19
+ attr_reader :x, :y, :z
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
+
35
+ # @overload initialize(options={})
36
+ # @option options [Radians] :angle Planar rotation angle
37
+ # @option options [Integer] :dimensions Dimensionality of the rotation
38
+ # @option options [Vector] :x X-axis
39
+ # @option options [Vector] :y Y-axis
40
+ # @option options [Vector] :z Z-axis
41
+ def initialize(*args)
42
+ options, args = args.partition {|a| a.is_a? Hash}
43
+ options = options.reduce({}, :merge)
44
+
45
+ @dimensions = options[:dimensions] || nil
46
+
47
+ axis_options = [options[:x], options[:y], options[:z]]
48
+ all_axes_options = [options[:x], options[:y], options[:z]].select {|a| a}
49
+ if all_axes_options.count != 0
50
+ @x = options[:x] || nil
51
+ @y = options[:y] || nil
52
+ @z = options[:z] || nil
53
+
54
+ raise ArgumentError, "All axis options must be Vectors" unless all_axes_options.all? {|a| a.is_a?(Vector) or a.is_a?(Array) }
55
+
56
+ raise ArgumentError, "All provided axes must be the same size" unless all_axes_options.all? {|a| a.size == all_axes_options.first.size}
57
+
58
+ @dimensions ||= all_axes_options.first.size
59
+
60
+ raise ArgumentError, "Dimensionality mismatch" unless all_axes_options.first.size <= @dimensions
61
+ if all_axes_options.first.size < @dimensions
62
+ @x, @y, @z = [@x, @y, @z].map {|a| (a && (a.size != 0) && (a.size < @dimensions)) ? Array.new(@dimensions) {|i| a[i] || 0 } : a }
63
+ end
64
+
65
+ raise ArgumentError, "Too many axes specified (expected #{@dimensions - 1} but got #{all_axes_options.size}" unless all_axes_options.size == (@dimensions - 1)
66
+ end
67
+ end
68
+
69
+ def eql?(other)
70
+ (self.x.eql? other.x) && (self.y.eql? other.y) && (self.z.eql? other.z)
71
+ end
72
+ alias :== :eql?
73
+
74
+ def identity?
75
+ (!@x && !@y && !@z) || ([@x, @y, @z].select {|a| a}.all? {|a| a.respond_to?(:magnitude) ? (1 == a.magnitude) : (1 == a.size)})
76
+ end
77
+
78
+ # @attribute [r] matrix
79
+ # @return [Matrix] the transformation {Matrix} representing the {Rotation}
80
+ def matrix
81
+ return nil unless [@x, @y, @z].compact.size >= 2
82
+
83
+ # Force all axes to be Vectors
84
+ x,y,z = [@x, @y, @z].map {|a| a.is_a?(Array) ? Vector[*a] : a}
85
+
86
+ # Force all axes to exist
87
+ if x and y
88
+ z = x ** y
89
+ elsif x and z
90
+ y = x ** z
91
+ elsif y and z
92
+ x = y ** z
93
+ end
94
+
95
+ rows = []
96
+ [x, y, z].each_with_index {|a, i| rows.push(a.to_a) if i < @dimensions }
97
+
98
+ raise ArgumentError, "Number of axes must match the dimensions of each axis" unless @dimensions == rows.size
99
+
100
+ Matrix[*rows]
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
189
+ end
190
+ end
@@ -0,0 +1,75 @@
1
+ require 'matrix'
2
+
3
+ module Geometry
4
+ =begin
5
+ An object representing the size of something.
6
+
7
+ Supports all of the familiar {Vector} methods as well as a few convenience
8
+ methods (width, height and depth).
9
+
10
+ == Usage
11
+
12
+ === Constructor
13
+ size = Geometry::Size[x,y,z]
14
+ =end
15
+
16
+ class Size < Vector
17
+ attr_reader :x, :y, :z
18
+
19
+ # Allow vector-style initialization, but override to support copy-init
20
+ # from Vector, Point or another Size
21
+ #
22
+ # @overload [](x,y,z,...)
23
+ # @overload [](Point)
24
+ # @overload [](Size)
25
+ # @overload [](Vector)
26
+ # @return [Size] A new {Size} object
27
+ def self.[](*array)
28
+ array = array[0].to_a unless array[0].is_a?(Numeric)
29
+ super *array
30
+ end
31
+
32
+ # Allow comparison with an Array, otherwise do the normal thing
33
+ def ==(other)
34
+ return @elements == other if other.is_a?(Array)
35
+ super other
36
+ end
37
+
38
+ def inspect
39
+ 'Size' + @elements.inspect
40
+ end
41
+ def to_s
42
+ 'Size' + @elements.to_s
43
+ end
44
+
45
+ # @return [Number] The size along the Z axis
46
+ def depth
47
+ z
48
+ end
49
+
50
+ # @return [Number] The size along the Y axis
51
+ def height
52
+ y
53
+ end
54
+
55
+ # @return [Number] The size along the X axis
56
+ def width
57
+ x
58
+ end
59
+
60
+ # @return [Number] X-component (width)
61
+ def x
62
+ @elements[0]
63
+ end
64
+
65
+ # @return [Number] Y-component (height)
66
+ def y
67
+ @elements[1]
68
+ end
69
+
70
+ # @return [Number] Z-component (depth)
71
+ def z
72
+ @elements[2]
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,70 @@
1
+ require_relative 'point'
2
+
3
+ module Geometry
4
+ =begin rdoc
5
+ An object repesenting a zero {Size} in N-dimensional space
6
+
7
+ A {SizeZero} object is a {Size} that will always compare equal to zero and unequal to
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}.
10
+ =end
11
+ class SizeZero
12
+ def eql?(other)
13
+ if other.respond_to? :all?
14
+ other.all? {|e| e.eql? 0}
15
+ else
16
+ other == 0
17
+ end
18
+ end
19
+ alias == eql?
20
+
21
+ def coerce(other)
22
+ if other.is_a? Numeric
23
+ [other, 0]
24
+ elsif other.is_a? Array
25
+ [other, Array.new(other.size,0)]
26
+ elsif other.is_a? Vector
27
+ [other, Vector[*Array.new(other.size,0)]]
28
+ else
29
+ [Size[other], Size[Array.new(other.size,0)]]
30
+ end
31
+ end
32
+
33
+ # @group Arithmetic
34
+
35
+ # @group Unary operators
36
+ def +@
37
+ self
38
+ end
39
+
40
+ def -@
41
+ self
42
+ end
43
+ # @endgroup
44
+
45
+ def +(other)
46
+ other
47
+ end
48
+
49
+ def -(other)
50
+ if other.respond_to? :-@
51
+ -other
52
+ elsif other.respond_to? :map
53
+ other.map {|a| -a }
54
+ end
55
+ end
56
+
57
+ def *(other)
58
+ self
59
+ end
60
+
61
+ def /(other)
62
+ raise OperationNotDefined unless other.is_a? Numeric
63
+ raise ZeroDivisionError if 0 == other
64
+ self
65
+ end
66
+ # @endgroup
67
+
68
+ end
69
+ end
70
+