aurora-geometry 0.0.2
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/.gitignore +6 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.markdown +105 -0
- data/Rakefile +24 -0
- data/aurora-geometry.gemspec +23 -0
- data/lib/geometry.rb +22 -0
- data/lib/geometry/arc.rb +94 -0
- data/lib/geometry/circle.rb +122 -0
- data/lib/geometry/cluster_factory.rb +15 -0
- data/lib/geometry/edge.rb +140 -0
- data/lib/geometry/line.rb +154 -0
- data/lib/geometry/obround.rb +238 -0
- data/lib/geometry/path.rb +67 -0
- data/lib/geometry/point.rb +163 -0
- data/lib/geometry/point_zero.rb +107 -0
- data/lib/geometry/polygon.rb +368 -0
- data/lib/geometry/polyline.rb +318 -0
- data/lib/geometry/rectangle.rb +378 -0
- data/lib/geometry/regular_polygon.rb +136 -0
- data/lib/geometry/rotation.rb +190 -0
- data/lib/geometry/size.rb +75 -0
- data/lib/geometry/size_zero.rb +70 -0
- data/lib/geometry/square.rb +113 -0
- data/lib/geometry/text.rb +24 -0
- data/lib/geometry/transformation.rb +171 -0
- data/lib/geometry/transformation/composition.rb +39 -0
- data/lib/geometry/triangle.rb +78 -0
- data/lib/geometry/vector.rb +34 -0
- data/test/geometry.rb +5 -0
- data/test/geometry/arc.rb +25 -0
- data/test/geometry/circle.rb +112 -0
- data/test/geometry/edge.rb +132 -0
- data/test/geometry/line.rb +132 -0
- data/test/geometry/obround.rb +25 -0
- data/test/geometry/path.rb +66 -0
- data/test/geometry/point.rb +258 -0
- data/test/geometry/point_zero.rb +177 -0
- data/test/geometry/polygon.rb +214 -0
- data/test/geometry/polyline.rb +266 -0
- data/test/geometry/rectangle.rb +154 -0
- data/test/geometry/regular_polygon.rb +120 -0
- data/test/geometry/rotation.rb +108 -0
- data/test/geometry/size.rb +97 -0
- data/test/geometry/size_zero.rb +153 -0
- data/test/geometry/square.rb +66 -0
- data/test/geometry/transformation.rb +169 -0
- data/test/geometry/transformation/composition.rb +49 -0
- data/test/geometry/triangle.rb +32 -0
- data/test/geometry/vector.rb +41 -0
- 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
|
+
|