geometry-in-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+