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,154 @@
1
+ require_relative 'cluster_factory'
2
+ require_relative 'point'
3
+
4
+ module Geometry
5
+
6
+ =begin rdoc
7
+ A cluster of objects representing a Line of infinite length
8
+
9
+ Supports two-point, slope-intercept, and point-slope initializer forms
10
+
11
+ == Usage
12
+
13
+ === Two-point constructors
14
+ line = Geometry::Line[[0,0], [10,10]]
15
+ line = Geometry::Line[Geometry::Point[0,0], Geometry::Point[10,10]]
16
+ line = Geometry::Line[Vector[0,0], Vector[10,10]]
17
+
18
+ === Slope-intercept constructors
19
+ Geometry::Line[Rational(3,4), 5] # Slope = 3/4, Intercept = 5
20
+ Geometry::Line[0.75, 5]
21
+
22
+ === Point-slope constructors
23
+ Geometry::Line(Geometry::Point[0,0], 0.75)
24
+ Geometry::Line(Vector[0,0], Rational(3,4))
25
+
26
+ === Special constructors (2D only)
27
+ Geometry::Line.horizontal(y=0)
28
+ Geometry::Line.vertical(x=0)
29
+ =end
30
+
31
+ class Line
32
+ include ClusterFactory
33
+
34
+ # @overload [](Array, Array)
35
+ # @return [TwoPointLine]
36
+ # @overload [](Point, Point)
37
+ # @return [TwoPointLine]
38
+ # @overload [](Vector, Vector)
39
+ # @return [TwoPointLine]
40
+ # @overload [](y-intercept, slope)
41
+ # @return [SlopeInterceptLine]
42
+ # @overload [](point, slope)
43
+ # @return [PointSlopeLine]
44
+ def self.[](*args)
45
+ if( 2 == args.size )
46
+ args.map! {|x| x.is_a?(Array) ? Point[*x] : x}
47
+
48
+ # If both args are Points, create a TwoPointLine
49
+ return TwoPointLine.new(*args) if args.all? {|x| x.is_a?(Vector)}
50
+
51
+ # If only the first arg is a Point, create a PointSlopeLine
52
+ return PointSlopeLine.new(*args) if args.first.is_a?(Vector)
53
+
54
+ # Otherise, create a SlopeInterceptLine
55
+ return SlopeInterceptLine.new(*args)
56
+ else
57
+ nil
58
+ end
59
+ end
60
+
61
+ # @overload new(from, to)
62
+ # @option options [Point] :from A starting {Point}
63
+ # @option options [Point] :to An end {Point}
64
+ # @return [TwoPointLine]
65
+ # @overload new(start, end)
66
+ # @option options [Point] :start A starting {Point}
67
+ # @option options [Point] :end An end {Point}
68
+ # @return [TwoPointLine]
69
+ def self.new(options={})
70
+ from = options[:from] || options[:start]
71
+ to = options[:end] || options[:to]
72
+
73
+ if from and to
74
+ TwoPointLine.new(from, to)
75
+ else
76
+ raise ArgumentError, "Start and end Points must be provided"
77
+ end
78
+ end
79
+
80
+ def self.horizontal(y_intercept=0)
81
+ SlopeInterceptLine.new(0, y_intercept)
82
+ end
83
+ def self.vertical(x_intercept=0)
84
+ SlopeInterceptLine.new(1/0.0, x_intercept)
85
+ end
86
+ end
87
+
88
+ # @private
89
+ class PointSlopeLine < Line
90
+ # @return [Number] the slope of the {Line}
91
+ attr_reader :slope
92
+
93
+ def initialize(point, slope)
94
+ @point = Point[point]
95
+ @slope = slope
96
+ end
97
+ def to_s
98
+ 'Line(' + @slope.to_s + ',' + @point.to_s + ')'
99
+ end
100
+ end
101
+
102
+ # @private
103
+ class SlopeInterceptLine < Line
104
+ # @return [Number] the slope of the {Line}
105
+ attr_reader :slope
106
+
107
+ def initialize(slope, intercept)
108
+ @slope = slope
109
+ @intercept = intercept
110
+ end
111
+
112
+ def horizontal?
113
+ 0 == @slope
114
+ end
115
+ def vertical?
116
+ (1/0.0) == @slope
117
+ end
118
+
119
+ def intercept(axis=:y)
120
+ case axis
121
+ when :x
122
+ vertical? ? @intercept : (horizontal? ? nil : (-@intercept/@slope))
123
+ when :y
124
+ vertical? ? nil : @intercept
125
+ end
126
+ end
127
+
128
+ def to_s
129
+ 'Line(' + @slope.to_s + ',' + @intercept.to_s + ')'
130
+ end
131
+ end
132
+
133
+ # @private
134
+ class TwoPointLine < Line
135
+ attr_reader :first, :last
136
+
137
+ def initialize(point0, point1)
138
+ @first, @last = [Point[point0], Point[point1]]
139
+ end
140
+ def inspect
141
+ 'Line(' + @first.inspect + ', ' + @last.inspect + ')'
142
+ end
143
+ alias :to_s :inspect
144
+
145
+ # @group Accessors
146
+ # !@attribute [r[ slope
147
+ # @return [Number] the slope of the {Line}
148
+ def slope
149
+ (last.y - first.y)/(last.x - first.x)
150
+ end
151
+ # @endgroup
152
+ end
153
+ end
154
+
@@ -0,0 +1,238 @@
1
+ require_relative 'cluster_factory'
2
+ require_relative 'point'
3
+
4
+ module Geometry
5
+
6
+ =begin
7
+ The {Obround} class cluster represents a rectangle with semicircular end caps
8
+
9
+ {http://en.wiktionary.org/wiki/obround}
10
+ =end
11
+
12
+ class Obround
13
+ include ClusterFactory
14
+
15
+ # @overload new(width, height)
16
+ # Creates a {Obround} of the given width and height, centered on the origin
17
+ # @param [Number] height Height
18
+ # @param [Number] width Width
19
+ # @return [CenteredObround]
20
+ # @overload new(size)
21
+ # Creates a {Obround} of the given {Size} centered on the origin
22
+ # @param [Size] size Width and height
23
+ # @return [CenteredObround]
24
+ # @overload new(point0, point1)
25
+ # Creates a {Obround} using the given {Point}s
26
+ # @param [Point] point0 A corner
27
+ # @param [Point] point1 The other corner
28
+ # @overload new(origin, size)
29
+ # Creates a {Obround} from the given origin and size
30
+ # @param [Point] origin Lower-left corner
31
+ # @param [Size] size Width and height
32
+ # @return [SizedObround]
33
+ # @overload new(left, bottom, right, top)
34
+ # Creates a {Obround} from the locations of each side
35
+ # @param [Number] left X-coordinate of the left side
36
+ # @param [Number] bottom Y-coordinate of the bottom edge
37
+ # @param [Number] right X-coordinate of the right side
38
+ # @param [Number] top Y-coordinate of the top edge
39
+ # @return [Obround]
40
+ def self.new(*args)
41
+ case args.size
42
+ when 1
43
+ CenteredObround.new(args[0])
44
+ when 2
45
+ if args.all? {|a| a.is_a?(Numeric) }
46
+ CenteredObround.new(Size[*args])
47
+ elsif args.all? {|a| a.is_a?(Array) || a.is_a?(Point) }
48
+ original_new(*args)
49
+ elsif (args[0].is_a?(Point) or args[0].is_a?(Array))and args[1].is_a?(Size)
50
+ SizedObround.new(*args)
51
+ else
52
+ raise ArgumentError, "Invalid arguments #{args}"
53
+ end
54
+ when 4
55
+ raise ArgumentError unless args.all? {|a| a.is_a?(Numeric)}
56
+ left, bottom, right, top = *args
57
+ original_new(Point[left, bottom], Point[right, top])
58
+ end
59
+ end
60
+
61
+ # Create a {Obround} using the given {Point}s
62
+ # @param [Point0] point0 The bottom-left corner (closest to the origin)
63
+ # @param [Point1] point1 The top-right corner (farthest from the origin)
64
+ def initialize(point0, point1)
65
+ point0, point1 = Point[point0], Point[point1]
66
+ raise(ArgumentError, "Point sizes must match") unless point0.size == point1.size
67
+
68
+ # Reorder the points to get lower-left and upper-right
69
+ if (point0.x > point1.x) && (point0.y > point1.y)
70
+ point0, point1 = point1, point0
71
+ else
72
+ p0x, p1x = [point0.x, point1.x].minmax
73
+ p0y, p1y = [point0.y, point1.y].minmax
74
+ point0 = Point[p0x, p0y]
75
+ point1 = Point[p1x, p1y]
76
+ end
77
+ @points = [point0, point1]
78
+ end
79
+
80
+ def eql?(other)
81
+ self.points == other.points
82
+ end
83
+ alias :== :eql?
84
+
85
+ # @group Accessors
86
+
87
+ # @return [Point] The {Obround}'s center
88
+ def center
89
+ min, max = @points.minmax {|a,b| a.y <=> b.y}
90
+ Point[(max.x+min.x)/2, (max.y+min.y)/2]
91
+ end
92
+
93
+ # @return [Array<Point>] The {Obround}'s four points (counterclockwise)
94
+ def points
95
+ point0, point2 = *@points
96
+ point1 = Point[point2.x, point0.y]
97
+ point3 = Point[point0.x, point2.y]
98
+ [point0, point1, point2, point3]
99
+ end
100
+
101
+ def origin
102
+ minx = @points.min {|a,b| a.x <=> b.x}
103
+ miny = @points.min {|a,b| a.y <=> b.y}
104
+ Point[minx.x, miny.y]
105
+ end
106
+
107
+ def height
108
+ min, max = @points.minmax {|a,b| a.y <=> b.y}
109
+ max.y - min.y
110
+ end
111
+
112
+ def width
113
+ min, max = @points.minmax {|a,b| a.x <=> b.x}
114
+ max.x - min.x
115
+ end
116
+ # @endgroup
117
+ end
118
+
119
+ class CenteredObround < Obround
120
+ # @return [Point] The {Obround}'s center
121
+ attr_accessor :center
122
+ attr_reader :origin
123
+ # @return [Size] The {Size} of the {Obround}
124
+ attr_accessor :size
125
+
126
+ # @overload new(width, height)
127
+ # Creates a {Obround} of the given width and height, centered on the origin
128
+ # @param [Number] height Height
129
+ # @param [Number] width Width
130
+ # @return [CenteredObround]
131
+ # @overload new(size)
132
+ # Creates a {Obround} of the given {Size} centered on the origin
133
+ # @param [Size] size Width and height
134
+ # @return [CenteredObround]
135
+ # @overload new(center, size)
136
+ # Creates a {Obround} with the given center point and size
137
+ # @param [Point] center
138
+ # @param [Size] size
139
+ def initialize(*args)
140
+ if args[0].is_a?(Size)
141
+ @center = Point[0,0]
142
+ @size = args[0]
143
+ elsif args[0].is_a?(Geometry::Point) and args[1].is_a?(Geometry::Size)
144
+ @center, @size = args[0,1]
145
+ elsif (2 == args.size) and args.all? {|a| a.is_a?(Numeric)}
146
+ @center = Point[0,0]
147
+ @size = Geometry::Size[*args]
148
+ end
149
+ end
150
+
151
+ def eql?(other)
152
+ (self.center == other.center) && (self.size == other.size)
153
+ end
154
+ alias :== :eql?
155
+
156
+ # @group Accessors
157
+ # @return [Array<Point>] The {Obround}'s four points (clockwise)
158
+ def points
159
+ point0 = @center - @size/2.0
160
+ point2 = @center + @size/2.0
161
+ point1 = Point[point0.x,point2.y]
162
+ point3 = Point[point2.x, point0.y]
163
+ [point0, point1, point2, point3]
164
+ end
165
+
166
+ def height
167
+ @size.height
168
+ end
169
+
170
+ def width
171
+ @size.width
172
+ end
173
+ # @endgroup
174
+ end
175
+
176
+ class SizedObround < Obround
177
+ # @return [Point] The {Obround}'s center
178
+ attr_reader :center
179
+ # @return [Point] The {Obround}'s origin
180
+ attr_accessor :origin
181
+ # @return [Size] The {Size} of the {Obround}
182
+ attr_accessor :size
183
+
184
+ # @overload new(width, height)
185
+ # Creates an {Obround} of the given width and height with its origin at [0,0]
186
+ # @param [Number] height Height
187
+ # @param [Number] width Width
188
+ # @return SizedObround
189
+ # @overload new(size)
190
+ # Creates an {Obround} of the given {Size} with its origin at [0,0]
191
+ # @param [Size] size Width and height
192
+ # @return SizedObround
193
+ # @overload new(origin, size)
194
+ # Creates an {Obround} with the given origin point and size
195
+ # @param [Point] origin
196
+ # @param [Size] size
197
+ # @return SizedObround
198
+ def initialize(*args)
199
+ if args[0].is_a?(Size)
200
+ @origin = Point[0,0]
201
+ @size = args[0]
202
+ elsif (args[0].is_a?(Point) or args[0].is_a?(Array)) and args[1].is_a?(Geometry::Size)
203
+ @origin, @size = Point[args[0]], args[1]
204
+ elsif (2 == args.size) and args.all? {|a| a.is_a?(Numeric)}
205
+ @origin = Point[0,0]
206
+ @size = Geometry::Size[*args]
207
+ end
208
+ end
209
+
210
+ def eql?(other)
211
+ (self.origin == other.origin) && (self.size == other.size)
212
+ end
213
+ alias :== :eql?
214
+
215
+ # @group Accessors
216
+ def center
217
+ @origin + @size/2
218
+ end
219
+
220
+ # @return [Array<Point>] The {Obround}'s four points (clockwise)
221
+ def points
222
+ point0 = @origin
223
+ point2 = @origin + @size
224
+ point1 = Point[point0.x,point2.y]
225
+ point3 = Point[point2.x, point0.y]
226
+ [point0, point1, point2, point3]
227
+ end
228
+
229
+ def height
230
+ @size.height
231
+ end
232
+
233
+ def width
234
+ @size.width
235
+ end
236
+ # @endgroup
237
+ end
238
+ end
@@ -0,0 +1,67 @@
1
+ require 'geometry/arc'
2
+ require 'geometry/edge'
3
+
4
+ module Geometry
5
+ =begin
6
+ An object representing a set of connected elements, each of which could be an
7
+ {Edge} or an {Arc}. Unlike a {Polygon}, a {Path} is not guaranteed to be closed.
8
+ =end
9
+ class Path
10
+ attr_reader :elements
11
+
12
+ # Construct a new Path from {Point}s, {Edge}s, and {Arc}s
13
+ # Successive {Point}s will be converted to {Edge}s.
14
+ def initialize(*args)
15
+ args.map! {|a| (a.is_a?(Array) or a.is_a?(Vector)) ? Point[a] : a}
16
+ args.each {|a| raise ArgumentError, "Unknown argument type #{a.class}" unless a.is_a?(Point) or a.is_a?(Edge) or a.is_a?(Arc) }
17
+
18
+ @elements = []
19
+
20
+ first = args.shift
21
+ push first if first.is_a?(Edge) or first.is_a?(Arc)
22
+
23
+ args.reduce(first) do |previous, n|
24
+ case n
25
+ when Point
26
+ case previous
27
+ when Point then push Edge.new(previous, n)
28
+ when Arc, Edge then push Edge.new(previous.last, n) unless previous.last == n
29
+ end
30
+ last
31
+ when Edge
32
+ case previous
33
+ when Point then push Edge.new(previous, n.first)
34
+ when Arc, Edge then push Edge.new(previous.last, n.first) unless previous.last == n.first
35
+ end
36
+ push(n).last
37
+ when Arc
38
+ case previous
39
+ when Point
40
+ if previous == n.first
41
+ raise ArgumentError, "Duplicated point before an Arc"
42
+ else
43
+ push Edge.new(previous, n.first)
44
+ end
45
+ when Arc, Edge
46
+ push Edge.new(previous.last, n.first) unless previous.last == n.first
47
+ end
48
+ push(n).last
49
+ else
50
+ raise ArgumentError, "Unsupported argument type: #{n}"
51
+ end
52
+ end
53
+ end
54
+
55
+ # @return [Geometry] The last element in the {Path}
56
+ def last
57
+ @elements.last
58
+ end
59
+
60
+ # Append a new geometry element to the {Path}
61
+ # @return [Path]
62
+ def push(arg)
63
+ @elements.push arg
64
+ self
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,163 @@
1
+ require 'matrix'
2
+
3
+ require_relative 'point_zero'
4
+
5
+ module Geometry
6
+ DimensionMismatch = Class.new(StandardError)
7
+ OperationNotDefined = Class.new(StandardError)
8
+
9
+ =begin rdoc
10
+ An object repesenting a Point in N-dimensional space
11
+
12
+ Supports all of the familiar Vector methods and adds convenience
13
+ accessors for those variables you learned to hate in your high school
14
+ geometry class (x, y, z).
15
+
16
+ == Usage
17
+
18
+ === Constructor
19
+ point = Geometry::Point[x,y]
20
+ =end
21
+ class Point < Vector
22
+ attr_reader :x, :y, :z
23
+
24
+ # Allow vector-style initialization, but override to support copy-init
25
+ # from Vector or another Point
26
+ #
27
+ # @overload [](x,y,z,...)
28
+ # @overload [](Array)
29
+ # @overload [](Point)
30
+ # @overload [](Vector)
31
+ def self.[](*array)
32
+ return array[0] if array[0].is_a?(Point) or array[0].is_a?(PointZero)
33
+ array = array[0] if array[0].is_a?(Array)
34
+ array = array[0].to_a if array[0].is_a?(Vector)
35
+ super *array
36
+ end
37
+
38
+ # Creates and returns a new {PointZero} instance. Or, a {Point} full of zeros if the size argument is given.
39
+ # @param size [Number] the size of the new {Point} full of zeros
40
+ # @return [PointZero] A new {PointZero} instance
41
+ def self.zero(size=nil)
42
+ size ? Point[Array.new(size, 0)] : PointZero.new
43
+ end
44
+
45
+ # Return a copy of the {Point}
46
+ def clone
47
+ Point[@elements.clone]
48
+ end
49
+
50
+ # Allow comparison with an Array, otherwise do the normal thing
51
+ def eql?(other)
52
+ if other.is_a?(Array)
53
+ @elements.eql? other
54
+ elsif other.is_a?(PointZero)
55
+ @elements.all? {|e| e.eql? 0 }
56
+ else
57
+ super other
58
+ end
59
+ end
60
+
61
+ # Allow comparison with an Array, otherwise do the normal thing
62
+ def ==(other)
63
+ if other.is_a?(Array)
64
+ @elements.eql? other
65
+ elsif other.is_a?(PointZero)
66
+ @elements.all? {|e| e.eql? 0 }
67
+ else
68
+ super other
69
+ end
70
+ end
71
+
72
+ # Combined comparison operator
73
+ # @return [Point] The <=> operator is applied to the elements of the arguments pairwise and the results are returned in a Point
74
+ def <=>(other)
75
+ Point[self.to_a.zip(other.to_a).map {|a,b| a <=> b}.compact]
76
+ end
77
+
78
+ def coerce(other)
79
+ case other
80
+ when Array then [Point[*other], self]
81
+ when Numeric then [Point[Array.new(self.size, other)], self]
82
+ when Vector then [Point[*other], self]
83
+ else
84
+ raise TypeError, "#{self.class} can't be coerced into #{other.class}"
85
+ end
86
+ end
87
+
88
+ def inspect
89
+ 'Point' + @elements.inspect
90
+ end
91
+ def to_s
92
+ 'Point' + @elements.to_s
93
+ end
94
+
95
+ # @group Accessors
96
+ # @param [Integer] i Index into the {Point}'s elements
97
+ # @return [Numeric] Element i (starting at 0)
98
+ def [](i)
99
+ @elements[i]
100
+ end
101
+
102
+ # @attribute [r] x
103
+ # @return [Numeric] X-component
104
+ def x
105
+ @elements[0]
106
+ end
107
+
108
+ # @attribute [r] y
109
+ # @return [Numeric] Y-component
110
+ def y
111
+ @elements[1]
112
+ end
113
+
114
+ # @attribute [r] z
115
+ # @return [Numeric] Z-component
116
+ def z
117
+ @elements[2]
118
+ end
119
+ # @endgroup
120
+
121
+ # @group Arithmetic
122
+
123
+ # @group Unary operators
124
+ def +@
125
+ self
126
+ end
127
+
128
+ def -@
129
+ Point[@elements.map {|e| -e }]
130
+ end
131
+ # @endgroup
132
+
133
+ def +(other)
134
+ case other
135
+ when Numeric
136
+ Point[@elements.map {|e| e + other}]
137
+ when PointZero, NilClass
138
+ self.dup
139
+ else
140
+ raise OperationNotDefined, "#{other.class} must respond to :size and :[]" unless other.respond_to?(:size) && other.respond_to?(:[])
141
+ raise DimensionMismatch, "Can't add #{other} to #{self}" if size != other.size
142
+ Point[Array.new(size) {|i| @elements[i] + other[i] }]
143
+ end
144
+ end
145
+
146
+ def -(other)
147
+ case other
148
+ when Numeric
149
+ Point[@elements.map {|e| e - other}]
150
+ when PointZero, NilClass
151
+ self.dup
152
+ else
153
+ raise OperationNotDefined, "#{other.class} must respond to :size and :[]" unless other.respond_to?(:size) && other.respond_to?(:[])
154
+ raise DimensionMismatch, "Can't subtract #{other} from #{self}" if size != other.size
155
+ Point[Array.new(size) {|i| @elements[i] - other[i] }]
156
+ end
157
+ end
158
+
159
+ # @endgroup
160
+
161
+ end
162
+ end
163
+