geometry-in-ruby 0.0.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.
- data/.gitignore +6 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.markdown +105 -0
- data/Rakefile +24 -0
- data/geometry-in-ruby.gemspec +23 -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/composition.rb +39 -0
- data/lib/geometry/transformation.rb +171 -0
- data/lib/geometry/triangle.rb +78 -0
- data/lib/geometry/vector.rb +34 -0
- data/lib/geometry.rb +22 -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/composition.rb +49 -0
- data/test/geometry/transformation.rb +169 -0
- data/test/geometry/triangle.rb +32 -0
- data/test/geometry/vector.rb +41 -0
- data/test/geometry.rb +5 -0
- metadata +117 -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
|
+
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require_relative 'point'
|
2
|
+
|
3
|
+
module Geometry
|
4
|
+
NotSquareError = Class.new(ArgumentError)
|
5
|
+
|
6
|
+
=begin
|
7
|
+
The {Square} class cluster is like the {Rectangle} class cluster, but not longer in one direction.
|
8
|
+
|
9
|
+
== Constructors
|
10
|
+
|
11
|
+
square = Square.new from:[1,2], to:[2,3] # Using two corners
|
12
|
+
square = Square.new origin:[3,4], size:5 # Using an origin point and a size
|
13
|
+
=end
|
14
|
+
class Square
|
15
|
+
attr_reader :origin
|
16
|
+
|
17
|
+
# Creates a {Square} given two {Point}s
|
18
|
+
# @option options [Point] :from A corner (ie. bottom-left)
|
19
|
+
# @option options [Point] :to The other corner (ie. top-right)
|
20
|
+
# @option options [Point] :origin The lower left corner
|
21
|
+
# @option options [Number] :size Bigness
|
22
|
+
def initialize(options={})
|
23
|
+
origin = options[:from] || options[:origin]
|
24
|
+
origin = origin ? Point[origin] : PointZero.new
|
25
|
+
|
26
|
+
if options.has_key? :to
|
27
|
+
point1 = options[:to]
|
28
|
+
elsif options.has_key? :size
|
29
|
+
point1 = origin + options[:size]
|
30
|
+
end
|
31
|
+
|
32
|
+
point1 = Point[point1]
|
33
|
+
raise(ArgumentError, "Point sizes must match (#{origin.size} != #{point1.size})") unless origin.is_a?(PointZero) || (origin.size == point1.size)
|
34
|
+
|
35
|
+
# Reorder the points to get lower-left and upper-right
|
36
|
+
minx, maxx = [origin.x, point1.x].minmax
|
37
|
+
miny, maxy = [origin.y, point1.y].minmax
|
38
|
+
@points = [Point[minx, miny], Point[maxx, maxy]]
|
39
|
+
|
40
|
+
raise(NotSquareError) if height != width
|
41
|
+
end
|
42
|
+
|
43
|
+
# !@group Accessors
|
44
|
+
# @attribute [r] origin
|
45
|
+
# @return [Point] The lower left corner
|
46
|
+
def origin
|
47
|
+
@points.first
|
48
|
+
end
|
49
|
+
|
50
|
+
def height
|
51
|
+
min, max = @points.minmax {|a,b| a.y <=> b.y}
|
52
|
+
max.y - min.y
|
53
|
+
end
|
54
|
+
|
55
|
+
def width
|
56
|
+
min, max = @points.minmax {|a,b| a.x <=> b.x}
|
57
|
+
max.x - min.x
|
58
|
+
end
|
59
|
+
# @endgroup
|
60
|
+
end
|
61
|
+
|
62
|
+
# A {Square} created with a center point and a size
|
63
|
+
class CenteredSquare < Square
|
64
|
+
# @attribute [r] center
|
65
|
+
# @return [Point] The center of the {Square}
|
66
|
+
attr_reader :center
|
67
|
+
|
68
|
+
# @param [Point] center The center point
|
69
|
+
# @param [Numeric] size The length of each side
|
70
|
+
def initialize(center, size)
|
71
|
+
@center = Point[center]
|
72
|
+
@size = size
|
73
|
+
end
|
74
|
+
|
75
|
+
# @group Accessors
|
76
|
+
# @attribute [r] origin
|
77
|
+
# @return [Point] The lower left corner
|
78
|
+
def origin
|
79
|
+
Point[@center.x - size/2, @center.y - size/2]
|
80
|
+
end
|
81
|
+
|
82
|
+
# @attribute [r] points
|
83
|
+
# @return [Array<Point>] The {Square}'s four points (counterclockwise)
|
84
|
+
def points
|
85
|
+
half_size = @size/2
|
86
|
+
minx = @center.x - half_size
|
87
|
+
maxx = @center.x + half_size
|
88
|
+
miny = @center.y - half_size
|
89
|
+
maxy = @center.y + half_size
|
90
|
+
|
91
|
+
[Point[minx,miny], Point[maxx, miny], Point[maxx, maxy], Point[minx,maxy]]
|
92
|
+
end
|
93
|
+
|
94
|
+
def height
|
95
|
+
@size
|
96
|
+
end
|
97
|
+
|
98
|
+
def width
|
99
|
+
@size
|
100
|
+
end
|
101
|
+
# @endgroup
|
102
|
+
end
|
103
|
+
|
104
|
+
# A {Square} created with an origin point and a size
|
105
|
+
class SizedSquare < Square
|
106
|
+
# @param [Point] origin The origin point (bottom-left corner)
|
107
|
+
# @param [Numeric] size The length of each side
|
108
|
+
def initialize(origin, size)
|
109
|
+
@origin = Point[origin]
|
110
|
+
@size = size
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'point'
|
2
|
+
|
3
|
+
module Geometry
|
4
|
+
|
5
|
+
class Text
|
6
|
+
|
7
|
+
# @return [Point] The point located in the top left corner of {Text}'s
|
8
|
+
# bounding box
|
9
|
+
attr_reader :position
|
10
|
+
|
11
|
+
# @return [String] The {Text}'s textual content
|
12
|
+
attr_reader :content
|
13
|
+
|
14
|
+
def initialize(position, content)
|
15
|
+
@position = Point[position]
|
16
|
+
@content = content
|
17
|
+
end
|
18
|
+
|
19
|
+
def eql?(other)
|
20
|
+
self.content == other.content
|
21
|
+
end
|
22
|
+
alias :== :eql?
|
23
|
+
end
|
24
|
+
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
|