euclidean 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,160 @@
1
+ require 'matrix'
2
+
3
+ module Euclidean
4
+ DimensionMismatch = Class.new(StandardError)
5
+ OperationNotDefined = Class.new(StandardError)
6
+
7
+ =begin rdoc
8
+ An object repesenting a Point in N-dimensional space
9
+
10
+ Supports all of the familiar Vector methods and adds convenience
11
+ accessors for those variables you learned to hate in your high school
12
+ geometry class (x, y, z).
13
+
14
+ == Usage
15
+
16
+ === Constructor
17
+ point = Geometry::Point[x,y]
18
+ =end
19
+ class Point < Vector
20
+ attr_reader :x, :y, :z
21
+
22
+ # Allow vector-style initialization, but override to support copy-init
23
+ # from Vector or another Point
24
+ #
25
+ # @overload [](x,y,z,...)
26
+ # @overload [](Array)
27
+ # @overload [](Point)
28
+ # @overload [](Vector)
29
+ def self.[](*array)
30
+ return array[0] if array[0].is_a?(Point) or array[0].is_a?(PointZero)
31
+ array = array[0] if array[0].is_a?(Array)
32
+ array = array[0].to_a if array[0].is_a?(Vector)
33
+ super *array
34
+ end
35
+
36
+ # Creates and returns a new {PointZero} instance. Or, a {Point} full of zeros if the size argument is given.
37
+ # @param size [Number] the size of the new {Point} full of zeros
38
+ # @return [PointZero] A new {PointZero} instance
39
+ def self.zero(size=nil)
40
+ size ? Point[Array.new(size, 0)] : PointZero.new
41
+ end
42
+
43
+ # Return a copy of the {Point}
44
+ def clone
45
+ Point[@elements.clone]
46
+ end
47
+
48
+ # Allow comparison with an Array, otherwise do the normal thing
49
+ def eql?(other)
50
+ if other.is_a?(Array)
51
+ @elements.eql? other
52
+ elsif other.is_a?(PointZero)
53
+ @elements.all? {|e| e.eql? 0 }
54
+ else
55
+ super other
56
+ end
57
+ end
58
+
59
+ # Allow comparison with an Array, otherwise do the normal thing
60
+ def ==(other)
61
+ if other.is_a?(Array)
62
+ @elements.eql? other
63
+ elsif other.is_a?(PointZero)
64
+ @elements.all? {|e| e.eql? 0 }
65
+ else
66
+ super other
67
+ end
68
+ end
69
+
70
+ # Combined comparison operator
71
+ # @return [Point] The <=> operator is applied to the elements of the arguments pairwise and the results are returned in a Point
72
+ def <=>(other)
73
+ Point[self.to_a.zip(other.to_a).map {|a,b| a <=> b}.compact]
74
+ end
75
+
76
+ def coerce(other)
77
+ case other
78
+ when Array then [Point[*other], self]
79
+ when Numeric then [Point[Array.new(self.size, other)], self]
80
+ when Vector then [Point[*other], self]
81
+ else
82
+ raise TypeError, "#{self.class} can't be coerced into #{other.class}"
83
+ end
84
+ end
85
+
86
+ def inspect
87
+ 'Point' + @elements.inspect
88
+ end
89
+
90
+ def to_s
91
+ 'Point' + @elements.to_s
92
+ end
93
+
94
+ # @group Accessors
95
+ # @param [Integer] i Index into the {Point}'s elements
96
+ # @return [Numeric] Element i (starting at 0)
97
+ def [](i)
98
+ @elements[i]
99
+ end
100
+
101
+ # @attribute [r] x
102
+ # @return [Numeric] X-component
103
+ def x
104
+ @elements[0]
105
+ end
106
+
107
+ # @attribute [r] y
108
+ # @return [Numeric] Y-component
109
+ def y
110
+ @elements[1]
111
+ end
112
+
113
+ # @attribute [r] z
114
+ # @return [Numeric] Z-component
115
+ def z
116
+ @elements[2]
117
+ end
118
+ # @endgroup
119
+
120
+ # @group Arithmetic
121
+
122
+ # @group Unary operators
123
+ def +@
124
+ self
125
+ end
126
+
127
+ def -@
128
+ Point[@elements.map {|e| -e }]
129
+ end
130
+ # @endgroup
131
+
132
+ def +(other)
133
+ case other
134
+ when Numeric
135
+ Point[@elements.map {|e| e + other}]
136
+ when PointZero, NilClass
137
+ self.dup
138
+ else
139
+ raise OperationNotDefined, "#{other.class} must respond to :size and :[]" unless other.respond_to?(:size) && other.respond_to?(:[])
140
+ raise DimensionMismatch, "Can't add #{other} to #{self}" if size != other.size
141
+ Point[Array.new(size) {|i| @elements[i] + other[i] }]
142
+ end
143
+ end
144
+
145
+ def -(other)
146
+ case other
147
+ when Numeric
148
+ Point[@elements.map {|e| e - other}]
149
+ when PointZero, NilClass
150
+ self.dup
151
+ else
152
+ raise OperationNotDefined, "#{other.class} must respond to :size and :[]" unless other.respond_to?(:size) && other.respond_to?(:[])
153
+ raise DimensionMismatch, "Can't subtract #{other} from #{self}" if size != other.size
154
+ Point[Array.new(size) {|i| @elements[i] - other[i] }]
155
+ end
156
+ end
157
+
158
+ # @endgroup
159
+ end
160
+ end
@@ -0,0 +1,104 @@
1
+ module Euclidean
2
+ =begin rdoc
3
+ An object repesenting a {Point} at the origin in N-dimensional space
4
+
5
+ A {PointZero} object is a {Point} that will always compare equal to zero and unequal to
6
+ everything else, regardless of size. You can think of it as an application of the
7
+ {http://en.wikipedia.org/wiki/Null_Object_pattern Null Object Pattern}.
8
+ =end
9
+ class PointZero
10
+
11
+ def eql?(other)
12
+ if other.respond_to? :all?
13
+ other.all? {|e| e.eql? 0}
14
+ else
15
+ other == 0
16
+ end
17
+ end
18
+ alias == eql?
19
+
20
+ def coerce(other)
21
+ if other.is_a? Numeric
22
+ [other, 0]
23
+ elsif other.is_a? Array
24
+ [other, Array.new(other.size,0)]
25
+ elsif other.is_a? Vector
26
+ [other, Vector[*Array.new(other.size,0)]]
27
+ else
28
+ [Point[other], Point[Array.new(other.size,0)]]
29
+ end
30
+ end
31
+
32
+ # This is a hack to get Array#== to work properly. It works on ruby 2.0 and 1.9.3.
33
+ def to_a
34
+ []
35
+ end
36
+
37
+ # @group Accessors
38
+ # @param [Integer] i Index into the {Point}'s elements
39
+ # @return [Numeric] Element i (starting at 0)
40
+ def [](i)
41
+ 0
42
+ end
43
+
44
+ # @attribute [r] x
45
+ # @return [Numeric] X-component
46
+ def x
47
+ 0
48
+ end
49
+
50
+ # @attribute [r] y
51
+ # @return [Numeric] Y-component
52
+ def y
53
+ 0
54
+ end
55
+
56
+ # @attribute [r] z
57
+ # @return [Numeric] Z-component
58
+ def z
59
+ 0
60
+ end
61
+ # @endgroup
62
+
63
+ # @group Arithmetic
64
+
65
+ # @group Unary operators
66
+ def +@
67
+ self
68
+ end
69
+
70
+ def -@
71
+ self
72
+ end
73
+ # @endgroup
74
+
75
+ def +(other)
76
+ case other
77
+ when Array, Numeric then other
78
+ else
79
+ Point[other]
80
+ end
81
+ end
82
+
83
+ def -(other)
84
+ if other.is_a? Size
85
+ -Point[other]
86
+ elsif other.respond_to? :-@
87
+ -other
88
+ elsif other.respond_to? :map
89
+ other.map {|a| -a }
90
+ end
91
+ end
92
+
93
+ def *(other)
94
+ self
95
+ end
96
+
97
+ def /(other)
98
+ raise OperationNotDefined unless other.is_a? Numeric
99
+ raise ZeroDivisionError if 0 == other
100
+ self
101
+ end
102
+ # @endgroup
103
+ end
104
+ end
@@ -0,0 +1,378 @@
1
+ require_relative 'edge'
2
+
3
+ module Euclidean
4
+ =begin
5
+ The {Rectangle} class cluster represents your typical arrangement of 4 corners and 4 sides.
6
+
7
+ == Usage
8
+
9
+ === Constructors
10
+ rect = Euclidean::Rectangle.new [1,2], [2,3] # Using two corners
11
+ rect = Euclidean::Rectangle.new from:[1,2], to:[2,3] # Using two corners
12
+
13
+ rect = Euclidean::Rectangle.new center:[1,2], size:[1,1] # Using a center point and a size
14
+ rect = Euclidean::Rectangle.new origin:[1,2], size:[1,1] # Using an origin point and a size
15
+
16
+ rect = Euclidean::Rectangle.new size: [10, 20] # origin = [0,0], size = [10, 20]
17
+ rect = Euclidean::Rectangle.new size: Size[10, 20] # origin = [0,0], size = [10, 20]
18
+ rect = Euclidean::Rectangle.new width: 10, height: 20 # origin = [0,0], size = [10, 20]
19
+ =end
20
+
21
+ class Rectangle
22
+ include ClusterFactory
23
+
24
+ # @return [Point] The {Rectangle}'s center
25
+ attr_reader :center
26
+ # @return [Number] Height of the {Rectangle}
27
+ attr_reader :height
28
+ # @return [Point] The {Rectangle}'s origin
29
+ attr_reader :origin
30
+ # @return [Size] The {Size} of the {Rectangle}
31
+ attr_reader :size
32
+ # @return [Number] Width of the {Rectangle}
33
+ attr_reader :width
34
+
35
+ # @overload new(width, height)
36
+ # Creates a {Rectangle} of the given width and height, centered on the origin
37
+ # @param [Number] height Height
38
+ # @param [Number] width Width
39
+ # @return [CenteredRectangle]
40
+ # @overload new(size)
41
+ # Creates a {Rectangle} of the given {Size} centered on the origin
42
+ # @param [Size] size Width and height
43
+ # @return [CenteredRectangle]
44
+ # @overload new(point0, point1)
45
+ # Creates a {Rectangle} using the given {Point}s
46
+ # @param [Point] point0 A corner
47
+ # @param [Point] point1 The other corner
48
+ # @overload new(origin, size)
49
+ # Creates a {Rectangle} from the given origin and size
50
+ # @param [Point] origin Lower-left corner
51
+ # @param [Size] size Width and height
52
+ # @return [SizedRectangle]
53
+ # @overload new(left, bottom, right, top)
54
+ # Creates a {Rectangle} from the locations of each side
55
+ # @param [Number] left X-coordinate of the left side
56
+ # @param [Number] bottom Y-coordinate of the bottom edge
57
+ # @param [Number] right X-coordinate of the right side
58
+ # @param [Number] top Y-coordinate of the top edge
59
+ def self.new(*args)
60
+ options, args = args.partition {|a| a.is_a? Hash}
61
+ options = options.reduce({}, :merge)
62
+
63
+ if options.has_key?(:size)
64
+ if options.has_key?(:center)
65
+ CenteredRectangle.new(center: options[:center], size: options[:size])
66
+ elsif options.has_key?(:origin)
67
+ SizedRectangle.new(origin: options[:origin], size: options[:size])
68
+ else
69
+ SizedRectangle.new(size: options[:size])
70
+ end
71
+ elsif options.has_key?(:from) and options.has_key?(:to)
72
+ original_new(options[:from], options[:to])
73
+ elsif options.has_key?(:height) and options.has_key?(:width)
74
+ SizedRectangle.new(height: options[:height], width: options[:width])
75
+ elsif (2==args.count) and (args.all? {|a| a.is_a?(Array) || a.is_a?(Point) })
76
+ original_new(*args)
77
+ elsif options.empty?
78
+ raise ArgumentError, "#{self} arguments must be named, not: #{args}"
79
+ else
80
+ raise ArgumentError, "Bad Rectangle arguments: #{args}, #{options}"
81
+ end
82
+ end
83
+
84
+ # Creates a {Rectangle} using the given {Point}s
85
+ # @param [Point] point0 A corner (ie. bottom-left)
86
+ # @param [Point] point1 The other corner (ie. top-right)
87
+ def initialize(point0, point1)
88
+ point0 = Point[point0]
89
+ point1 = Point[point1]
90
+ raise(ArgumentError, "Point sizes must match") unless point0.size == point1.size
91
+
92
+ # Reorder the points to get lower-left and upper-right
93
+ if (point0.x > point1.x) && (point0.y > point1.y)
94
+ point0, point1 = point1, point0
95
+ else
96
+ p0x, p1x = [point0.x, point1.x].minmax
97
+ p0y, p1y = [point0.y, point1.y].minmax
98
+ point0 = Point[p0x, p0y]
99
+ point1 = Point[p1x, p1y]
100
+ end
101
+ @points = [point0, point1]
102
+ end
103
+
104
+ def eql?(other)
105
+ self.points == other.points
106
+ end
107
+ alias :== :eql?
108
+
109
+ # @group Accessors
110
+
111
+ # @return [Rectangle] The smallest axis-aligned {Rectangle} that bounds the receiver
112
+ def bounds
113
+ return Rectangle.new(self.min, self.max)
114
+ end
115
+
116
+ # @return [Point] The {Rectangle}'s center
117
+ def center
118
+ min, max = @points.minmax {|a,b| a.y <=> b.y}
119
+ Point[(max.x+min.x)/2, (max.y+min.y)/2]
120
+ end
121
+
122
+ # @return [Array<Edge>] The {Rectangle}'s four edges (counterclockwise)
123
+ def edges
124
+ point0, point2 = *@points
125
+ point1 = Point[point2.x, point0.y]
126
+ point3 = Point[point0.x, point2.y]
127
+ [Edge.new(point0, point1),
128
+ Edge.new(point1, point2),
129
+ Edge.new(point2, point3),
130
+ Edge.new(point3, point0)]
131
+ end
132
+
133
+ def height
134
+ min, max = @points.minmax {|a,b| a.y <=> b.y}
135
+ max.y - min.y
136
+ end
137
+
138
+ # @return [Point] The upper right corner of the bounding {Rectangle}
139
+ def max
140
+ @points.last
141
+ end
142
+
143
+ # @return [Point] The lower left corner of the bounding {Rectangle}
144
+ def min
145
+ @points.first
146
+ end
147
+
148
+ # @return [Array<Point>] The lower left and upper right corners of the bounding {Rectangle}
149
+ def minmax
150
+ [self.min, self.max]
151
+ end
152
+
153
+ def origin
154
+ minx = @points.min {|a,b| a.x <=> b.x}
155
+ miny = @points.min {|a,b| a.y <=> b.y}
156
+ Point[minx.x, miny.y]
157
+ end
158
+
159
+ # @return [Array<Point>] The {Rectangle}'s four points (counterclockwise)
160
+ def points
161
+ point0, point2 = *@points
162
+ point1 = Point[point2.x, point0.y]
163
+ point3 = Point[point0.x, point2.y]
164
+ [point0, point1, point2, point3]
165
+ end
166
+
167
+ def width
168
+ min, max = @points.minmax {|a,b| a.x <=> b.x}
169
+ max.x - min.x
170
+ end
171
+ # @endgroup
172
+
173
+ # Create a new {Rectangle} from the receiver that's inset by the given amount
174
+ # @overload inset(x, y)
175
+ # @overload inset(top, left, bottom, right)
176
+ # @overload inset(x, y)
177
+ # @option options [Number] :x Inset from the left and right sides
178
+ # @option options [Number] :y Inset from the top and bottom
179
+ # @overload inset(top, left, bottom, right)
180
+ # @option options [Number] :bottom The inset from the bottom of the {Rectangle}
181
+ # @option options [Number] :left The inset from the left side of the {Rectangle}
182
+ # @option options [Number] :right The inset from the right side of the {Rectangle}
183
+ # @option options [Number] :top The inset from the top of the {Rectangle}
184
+ def inset(*args)
185
+ options, args = args.partition {|a| a.is_a? Hash}
186
+ options = options.reduce({}, :merge)
187
+ raise ArumentError, "Can't specify both arguments and options" if !args.empty? && !options.empty?
188
+
189
+ if 1 == args.size
190
+ distance = args.shift
191
+ Rectangle.new from:(min + distance), to:(max - distance)
192
+ elsif 2 == args.size
193
+ distance = Point[*args]
194
+ Rectangle.new from:(min + distance), to:(max - distance)
195
+ elsif 4 == args.size
196
+ top, left, bottom, right = *args
197
+ Rectangle.new from:(min + Point[left, bottom]), to:(max - Point[right, top])
198
+ elsif options[:x] && options[:y]
199
+ distance = Point[options[:x], options[:y]]
200
+ Rectangle.new from:(min + distance), to:(max - distance)
201
+ elsif options[:top] && options[:left] && options[:bottom] && options[:right]
202
+ Rectangle.new from:(min + Point[options[:left], options[:bottom]]), to:(max - Point[options[:right], options[:top]])
203
+ end
204
+ end
205
+
206
+ end
207
+
208
+ class CenteredRectangle < Rectangle
209
+ # @return [Point] The {Rectangle}'s center
210
+ attr_accessor :center
211
+ # @return [Size] The {Size} of the {Rectangle}
212
+ attr_accessor :size
213
+
214
+ # @overload new(width, height)
215
+ # Creates a {Rectangle} of the given width and height, centered on the origin
216
+ # @param [Number] height Height
217
+ # @param [Number] width Width
218
+ # @return [CenteredRectangle]
219
+ # @overload new(size)
220
+ # Creates a {Rectangle} of the given {Size} centered on the origin
221
+ # @param [Size] size Width and height
222
+ # @return [CenteredRectangle]
223
+ # @overload new(center, size)
224
+ # Creates a {Rectangle} with the given center point and size
225
+ # @param [Point] center
226
+ # @param [Size] size
227
+ def initialize(*args)
228
+ options, args = args.partition {|a| a.is_a? Hash}
229
+ options = options.reduce({}, :merge)
230
+
231
+ @center = options[:center] ? Point[options[:center]] : PointZero.new
232
+
233
+ if options.has_key?(:size)
234
+ @size = Euclidean::Size[options[:size]]
235
+ elsif options.has_key?(:height) and options.has_key?(:width)
236
+ @size = Euclidean::Size[options[:width], options[:height]]
237
+ else
238
+ raise ArgumentError, "Bad arguments to CenteredRectangle#new"
239
+ end
240
+ end
241
+
242
+ def eql?(other)
243
+ (self.center == other.center) && (self.size == other.size)
244
+ end
245
+ alias :== :eql?
246
+
247
+ # @group Accessors
248
+ # @return [Array<Edge>] The {Rectangle}'s four edges
249
+ def edges
250
+ point0 = @center - @size/2.0
251
+ point2 = @center + @size/2.0
252
+ point1 = Point[point0.x,point2.y]
253
+ point3 = Point[point2.x, point0.y]
254
+ [Edge.new(point0, point1),
255
+ Edge.new(point1, point2),
256
+ Edge.new(point2, point3),
257
+ Edge.new(point3, point0)]
258
+ end
259
+
260
+ def height
261
+ @size.height
262
+ end
263
+
264
+ # @return [Point] The upper right corner of the bounding {Rectangle}
265
+ def max
266
+ @center + @size/2.0
267
+ end
268
+
269
+ # @return [Point] The lower left corner of the bounding {Rectangle}
270
+ def min
271
+ @center - @size/2.0
272
+ end
273
+
274
+ # @return [Array<Point>] The {Rectangle}'s four points (clockwise)
275
+ def points
276
+ point0 = @center - @size/2.0
277
+ point2 = @center + @size/2.0
278
+ point1 = Point[point0.x,point2.y]
279
+ point3 = Point[point2.x, point0.y]
280
+ [point0, point1, point2, point3]
281
+ end
282
+
283
+ def width
284
+ @size.width
285
+ end
286
+ # @endgroup
287
+
288
+ end
289
+
290
+ class SizedRectangle < Rectangle
291
+ # @return [Point] The {Rectangle}'s origin
292
+ attr_accessor :origin
293
+ # @return [Size] The {Size} of the {Rectangle}
294
+ attr_accessor :size
295
+
296
+ # @overload new(width, height)
297
+ # Creates a {Rectangle} of the given width and height with its origin at [0,0]
298
+ # @param [Number] height Height
299
+ # @param [Number] width Width
300
+ # @return SizedRectangle
301
+ # @overload new(size)
302
+ # Creates a {Rectangle} of the given {Size} with its origin at [0,0]
303
+ # @param [Size] size Width and height
304
+ # @return SizedRectangle
305
+ # @overload new(origin, size)
306
+ # Creates a {Rectangle} with the given origin point and size
307
+ # @param [Point] origin
308
+ # @param [Size] size
309
+ # @return SizedRectangle
310
+ def initialize(*args)
311
+ options, args = args.partition {|a| a.is_a? Hash}
312
+ options = options.reduce({}, :merge)
313
+
314
+ @origin = options[:origin] ? Point[options[:origin]] : PointZero.new
315
+
316
+ if options.has_key?(:size)
317
+ @size = Euclidean::Size[options[:size]]
318
+ elsif options.has_key?(:height) and options.has_key?(:width)
319
+ @size = Euclidean::Size[options[:width], options[:height]]
320
+ else
321
+ raise ArgumentError, "Bad arguments to SizeRectangle#new"
322
+ end
323
+ end
324
+
325
+ def eql?(other)
326
+ (self.origin == other.origin) && (self.size == other.size)
327
+ end
328
+ alias :== :eql?
329
+
330
+ # @group Accessors
331
+ # @return [Point] The {Rectangle}'s center
332
+ def center
333
+ @origin + @size/2
334
+ end
335
+
336
+ # @return [Array<Edge>] The {Rectangle}'s four edges
337
+ def edges
338
+ point0 = @origin
339
+ point2 = @origin + @size
340
+ point1 = Point[point0.x,point2.y]
341
+ point3 = Point[point2.x, point0.y]
342
+ [Edge.new(point0, point1),
343
+ Edge.new(point1, point2),
344
+ Edge.new(point2, point3),
345
+ Edge.new(point3, point0)]
346
+ end
347
+
348
+ def height
349
+ @size.height
350
+ end
351
+
352
+ # @return [Point] The upper right corner of the bounding {Rectangle}
353
+ def max
354
+ @origin + @size
355
+ end
356
+
357
+ # @return [Point] The lower left corner of the bounding {Rectangle}
358
+ def min
359
+ @origin
360
+ end
361
+
362
+ # @return [Array<Point>] The {Rectangle}'s four points (clockwise)
363
+ def points
364
+ point0 = @origin
365
+ point2 = @origin + @size
366
+ point1 = Point[point0.x,point2.y]
367
+ point3 = Point[point2.x, point0.y]
368
+ [point0, point1, point2, point3]
369
+ end
370
+
371
+ def width
372
+ @size.width
373
+ end
374
+ # @endgroup
375
+
376
+ end
377
+
378
+ end