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.
Files changed (51) hide show
  1. data/.gitignore +6 -0
  2. data/Gemfile +7 -0
  3. data/LICENSE +21 -0
  4. data/README.markdown +105 -0
  5. data/Rakefile +24 -0
  6. data/geometry-in-ruby.gemspec +23 -0
  7. data/lib/geometry/arc.rb +94 -0
  8. data/lib/geometry/circle.rb +122 -0
  9. data/lib/geometry/cluster_factory.rb +15 -0
  10. data/lib/geometry/edge.rb +140 -0
  11. data/lib/geometry/line.rb +154 -0
  12. data/lib/geometry/obround.rb +238 -0
  13. data/lib/geometry/path.rb +67 -0
  14. data/lib/geometry/point.rb +163 -0
  15. data/lib/geometry/point_zero.rb +107 -0
  16. data/lib/geometry/polygon.rb +368 -0
  17. data/lib/geometry/polyline.rb +318 -0
  18. data/lib/geometry/rectangle.rb +378 -0
  19. data/lib/geometry/regular_polygon.rb +136 -0
  20. data/lib/geometry/rotation.rb +190 -0
  21. data/lib/geometry/size.rb +75 -0
  22. data/lib/geometry/size_zero.rb +70 -0
  23. data/lib/geometry/square.rb +113 -0
  24. data/lib/geometry/text.rb +24 -0
  25. data/lib/geometry/transformation/composition.rb +39 -0
  26. data/lib/geometry/transformation.rb +171 -0
  27. data/lib/geometry/triangle.rb +78 -0
  28. data/lib/geometry/vector.rb +34 -0
  29. data/lib/geometry.rb +22 -0
  30. data/test/geometry/arc.rb +25 -0
  31. data/test/geometry/circle.rb +112 -0
  32. data/test/geometry/edge.rb +132 -0
  33. data/test/geometry/line.rb +132 -0
  34. data/test/geometry/obround.rb +25 -0
  35. data/test/geometry/path.rb +66 -0
  36. data/test/geometry/point.rb +258 -0
  37. data/test/geometry/point_zero.rb +177 -0
  38. data/test/geometry/polygon.rb +214 -0
  39. data/test/geometry/polyline.rb +266 -0
  40. data/test/geometry/rectangle.rb +154 -0
  41. data/test/geometry/regular_polygon.rb +120 -0
  42. data/test/geometry/rotation.rb +108 -0
  43. data/test/geometry/size.rb +97 -0
  44. data/test/geometry/size_zero.rb +153 -0
  45. data/test/geometry/square.rb +66 -0
  46. data/test/geometry/transformation/composition.rb +49 -0
  47. data/test/geometry/transformation.rb +169 -0
  48. data/test/geometry/triangle.rb +32 -0
  49. data/test/geometry/vector.rb +41 -0
  50. data/test/geometry.rb +5 -0
  51. 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