euclidean 0.1.0

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.
@@ -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