geometry 5 → 6

Sign up to get free protection for your applications and to get access to all the features.
data/lib/geometry/path.rb CHANGED
@@ -18,38 +18,50 @@ An object representing a set of connected elements, each of which could be an
18
18
  @elements = []
19
19
 
20
20
  first = args.shift
21
- @elements.push first if first.is_a?(Edge) or first.is_a?(Arc)
21
+ push first if first.is_a?(Edge) or first.is_a?(Arc)
22
22
 
23
23
  args.reduce(first) do |previous, n|
24
24
  case n
25
25
  when Point
26
26
  case previous
27
- when Point then @elements.push Edge.new(previous, n)
28
- when Arc, Edge then @elements.push Edge.new(previous.last, n) unless previous.last == n
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
29
  end
30
- @elements.last
30
+ last
31
31
  when Edge
32
32
  case previous
33
- when Point then @elements.push Edge.new(previous, n.first)
34
- when Arc, Edge then @elements.push Edge.new(previous.last, n.first) unless previous.last == n.first
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
35
  end
36
- @elements.push(n).last
36
+ push(n).last
37
37
  when Arc
38
38
  case previous
39
39
  when Point
40
40
  if previous == n.first
41
41
  raise ArgumentError, "Duplicated point before an Arc"
42
42
  else
43
- @elements.push Edge.new(previous, n.first)
43
+ push Edge.new(previous, n.first)
44
44
  end
45
45
  when Arc, Edge
46
- @elements.push Edge.new(previous.last, n.first) unless previous.last == n.first
46
+ push Edge.new(previous.last, n.first) unless previous.last == n.first
47
47
  end
48
- @elements.push(n).last
48
+ push(n).last
49
49
  else
50
50
  raise ArgumentError, "Unsupported argument type: #{n}"
51
51
  end
52
52
  end
53
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
54
66
  end
55
67
  end
@@ -1,5 +1,7 @@
1
1
  require 'matrix'
2
2
 
3
+ require_relative 'point_zero'
4
+
3
5
  module Geometry
4
6
  DimensionMismatch = Class.new(StandardError)
5
7
  OperationNotDefined = Class.new(StandardError)
@@ -23,10 +25,11 @@ geometry class (x, y, z).
23
25
  # from Vector or another Point
24
26
  #
25
27
  # @overload [](x,y,z,...)
28
+ # @overload [](Array)
26
29
  # @overload [](Point)
27
30
  # @overload [](Vector)
28
31
  def self.[](*array)
29
- return array[0] if array[0].is_a? Point
32
+ return array[0] if array[0].is_a?(Point) or array[0].is_a?(PointZero)
30
33
  array = array[0] if array[0].is_a?(Array)
31
34
  array = array[0].to_a if array[0].is_a?(Vector)
32
35
  super *array
@@ -40,14 +43,24 @@ geometry class (x, y, z).
40
43
 
41
44
  # Allow comparison with an Array, otherwise do the normal thing
42
45
  def eql?(other)
43
- return @elements.eql? other if other.is_a?(Array)
44
- super other
46
+ if other.is_a?(Array)
47
+ @elements.eql? other
48
+ elsif other.is_a?(PointZero)
49
+ @elements.all? {|e| e.eql? 0 }
50
+ else
51
+ super other
52
+ end
45
53
  end
46
54
 
47
55
  # Allow comparison with an Array, otherwise do the normal thing
48
56
  def ==(other)
49
- return @elements == other if other.is_a?(Array)
50
- super other
57
+ if other.is_a?(Array)
58
+ @elements.eql? other
59
+ elsif other.is_a?(PointZero)
60
+ @elements.all? {|e| e.eql? 0 }
61
+ else
62
+ super other
63
+ end
51
64
  end
52
65
 
53
66
  # Combined comparison operator
@@ -59,6 +72,7 @@ geometry class (x, y, z).
59
72
  def coerce(other)
60
73
  case other
61
74
  when Array then [Point[*other], self]
75
+ when Numeric then [Point[Array.new(self.size, other)], self]
62
76
  when Vector then [Point[*other], self]
63
77
  else
64
78
  raise TypeError, "#{self.class} can't be coerced into #{other.class}"
@@ -113,8 +127,7 @@ geometry class (x, y, z).
113
127
  def +(other)
114
128
  case other
115
129
  when Numeric
116
- raise DimensionMismatch, "A scalar can't be added to a Point of dimension greater than 1" if size != 1
117
- Point[@elements.first + other]
130
+ Point[@elements.map {|e| e + other}]
118
131
  when PointZero
119
132
  self
120
133
  else
@@ -127,8 +140,7 @@ geometry class (x, y, z).
127
140
  def -(other)
128
141
  case other
129
142
  when Numeric
130
- raise DimensionMismatch, "A scalar can't be subtracted from a Point of dimension greater than 1" if size != 1
131
- Point[@elements.first - other]
143
+ Point[@elements.map {|e| e - other}]
132
144
  when PointZero
133
145
  self
134
146
  else
@@ -29,6 +29,37 @@ everything else, regardless of size.
29
29
  end
30
30
  end
31
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_ary
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
+
32
63
  # @group Arithmetic
33
64
 
34
65
  # @group Unary operators
@@ -42,11 +73,17 @@ everything else, regardless of size.
42
73
  # @endgroup
43
74
 
44
75
  def +(other)
45
- other
76
+ case other
77
+ when Array, Numeric then other
78
+ else
79
+ Point[other]
80
+ end
46
81
  end
47
82
 
48
83
  def -(other)
49
- if other.respond_to? :-@
84
+ if other.is_a? Size
85
+ -Point[other]
86
+ elsif other.respond_to? :-@
50
87
  -other
51
88
  elsif other.respond_to? :map
52
89
  other.map {|a| -a }
@@ -8,6 +8,9 @@ A {Polygon} is a closed path comprised entirely of lines so straight they don't
8
8
 
9
9
  {http://en.wikipedia.org/wiki/Polygon}
10
10
 
11
+ The {Polygon} class is generally intended to represent {http://en.wikipedia.org/wiki/Simple_polygon Simple polygons},
12
+ but there's currently nothing that enforces simplicity.
13
+
11
14
  == Usage
12
15
 
13
16
  =end
@@ -31,6 +34,137 @@ A {Polygon} is a closed path comprised entirely of lines so straight they don't
31
34
  @edges.push Edge.new(@edges.last.last, @edges.first.first) unless @edges.empty? || (@edges.last.last == @edges.first.first)
32
35
  end
33
36
 
37
+ # Check the orientation of the {Polygon}
38
+ # @return [Boolean] True if the {Polygon} is clockwise, otherwise false
39
+ def clockwise?
40
+ edges.map {|e| (e.last.x - e.first.x) * (e.last.y + e.first.y)}.reduce(:+) >= 0
41
+ end
42
+
43
+ # @return [Polygon] A new {Polygon} with orientation that's the opposite of the receiver
44
+ def reverse
45
+ self.class.new *(self.vertices.reverse)
46
+ end
47
+
48
+ # @group Boolean operators
49
+
50
+ # Test a {Point} for inclusion in the receiver using a simplified winding number algorithm
51
+ # @param [Point] point The {Point} to test
52
+ # @return [Number] 1 if the {Point} is inside the {Polygon}, -1 if it's outside, and 0 if it's on an {Edge}
53
+ def <=>(point)
54
+ sum = edges.reduce(0) do |sum, e|
55
+ direction = e.last.y <=> e.first.y
56
+ # Ignore edges that don't cross the point's x coordinate
57
+ next sum unless ((point.y <=> e.last.y) + (point.y <=> e.first.y)).abs <= 1
58
+
59
+ if 0 == direction # Special case horizontal edges
60
+ return 0 if ((point.x <=> e.last.x) + (point.x <=> e.first.x)).abs <= 1
61
+ next sum # Doesn't intersect
62
+ else
63
+ is_left = e <=> point
64
+ return 0 if 0 == is_left
65
+ next sum unless is_left
66
+ sum += 0 <=> (direction + is_left)
67
+ end
68
+ end
69
+ (0 == sum) ? -1 : 1
70
+ end
71
+
72
+ # Create a new {Polygon} that's the union of the receiver and a passed {Polygon}
73
+ # This is a simplified implementation of the alogrithm outlined in the
74
+ # paper {http://gvu.gatech.edu/people/official/jarek/graphics/papers/04PolygonBooleansMargalit.pdf An algorithm for computing the union, intersection or difference of two polygons}.
75
+ # In particular, this method assumes the receiver and passed {Polygon}s are "island" type and that the desired output is "regular", as those terms are described in the paper.
76
+ # @param [Polygon] other The {Polygon} to union with the receiver
77
+ # @return [Polygon] The union of the receiver and the passed {Polygon}
78
+ def union(other)
79
+ # Table 1: Both polygons are islands and the operation is union, so both must have the same orientation
80
+ # Reverse the other polygon if the orientations are different
81
+ other = other.reverse if self.clockwise? != other.clockwise?
82
+
83
+ # Receiver's vertex ring
84
+ ringA = VertexRing.new
85
+ self.vertices.each {|v| ringA.push v, (other <=> v)}
86
+
87
+ # The other vertex ring
88
+ ringB = VertexRing.new
89
+ other.vertices.each {|v| ringB.push v, (self <=> v)}
90
+
91
+ # Find intersections
92
+ offsetA = 0
93
+ edgesB = other.edges.dup
94
+ self.edges.each_with_index do |a, indexA|
95
+ offsetB = 0
96
+ ringB.edges_with_index do |b, indexB|
97
+ intersection = a.intersection(b)
98
+ if intersection === true
99
+ if (a.first == b.first) and (a.last == b.last) # Equal edges
100
+ elsif (a.first == b.last) and (a.last == b.first) # Ignore equal but opposite edges
101
+ else
102
+ if a.vector.normalize == b.vector.normalize # Same direction?
103
+ offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, b.first)
104
+ offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, a.last)
105
+ else # Opposite direction
106
+ offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, b.last)
107
+ offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, a.first)
108
+ end
109
+ end
110
+ elsif intersection.is_a?(Point)
111
+ offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, intersection)
112
+ offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, intersection)
113
+ end
114
+ end
115
+ end
116
+
117
+ # Table 2: Both polygons are islands and the operation is union, so select outside from both polygons
118
+ edgeFragments = []
119
+ [[ringA, other], [ringB, self]].each do |ring, other_polygon|
120
+ ring.edges do |v1,v2|
121
+ if (v1[:type] == -1) or (v2[:type] == -1)
122
+ edgeFragments.push :first => v1[:vertex], :last => v2[:vertex]
123
+ elsif (v1[:type] == 0) and (v2[:type] == 0)
124
+ if (other_polygon <=> Point[(v1[:vertex] + v2[:vertex])/2]) <= 0
125
+ edgeFragments.push :first => v1[:vertex], :last => v2[:vertex]
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ # Delete any duplicated edges. Array#uniq doesn't do the right thing, so using inject instead.
132
+ edgeFragments = edgeFragments.inject([]) {|result,h| result << h unless result.include?(h); result}
133
+
134
+ # Delete any equal-and-opposite edges
135
+ edgeFragments = edgeFragments.reject {|f| edgeFragments.find {|f2| (f[:first] == f2[:last]) and (f[:last] == f2[:first])} }
136
+
137
+ # Construct the output polygons
138
+ output = edgeFragments.reduce([Array.new]) do |output, fragment|
139
+ next output if fragment.empty?
140
+ polygon = output.last
141
+ polygon.push fragment[:first], fragment[:last] if polygon.empty?
142
+ while 1 do
143
+ adjacent_fragment = edgeFragments.find {|f| fragment[:last] == f[:first]}
144
+ break unless adjacent_fragment
145
+
146
+ polygon.push adjacent_fragment[:first], adjacent_fragment[:last]
147
+ fragment = adjacent_fragment.dup
148
+ adjacent_fragment.clear
149
+
150
+ break if polygon.first == polygon.last # closed?
151
+ end
152
+ output << Array.new
153
+ end
154
+
155
+ # If everything worked properly there should be only one output Polygon
156
+ output.reject! {|a| a.empty?}
157
+ output = Polygon.new *(output[0])
158
+
159
+ # Table 4: Both input polygons are "island" type and the operation
160
+ # is union, so the output polygon's orientation should be the same
161
+ # as the input polygon's orientation
162
+ (self.clockwise? != output.clockwise?) ? output.reverse : output
163
+ end
164
+ alias :+ :union
165
+
166
+ # @endgroup
167
+
34
168
  # @group Convex Hull
35
169
 
36
170
  # Returns the convex hull of the {Polygon}
@@ -179,4 +313,50 @@ A {Polygon} is a closed path comprised entirely of lines so straight they don't
179
313
  dx / (dx + dy)
180
314
  end
181
315
  end
316
+
317
+ private
318
+
319
+ class VertexRing
320
+ attr_reader :vertices
321
+
322
+ def initialize
323
+ @vertices = []
324
+ end
325
+
326
+ # @param [Integer] index The index to insert the new {Point} before
327
+ # @param [Point] point The {Point} to insert
328
+ # @param [Integer] type The vertex type: 1 is inside, 0 is boundary, -1 is outside
329
+ def insert(index, point, type)
330
+ if v = @vertices.find {|v| v[:vertex] == point }
331
+ v[:type] = type
332
+ false
333
+ else
334
+ @vertices.insert(index, {:vertex => point, :type => type})
335
+ true
336
+ end
337
+ end
338
+
339
+ # Insert a boundary vertex
340
+ # @param [Integer] index The index to insert the new {Point} before
341
+ # @param [Point] point The {Point} to insert
342
+ def insert_boundary(index, point)
343
+ self.insert(index, point, 0)
344
+ end
345
+
346
+ # @param [Point] point The {Point} to push
347
+ # @param [Integer] type The vertex type: 1 is inside, 0 is boundary, -1 is outside
348
+ def push(point, type)
349
+ @vertices << {:vertex => point, :type => type}
350
+ end
351
+
352
+ # Enumerate the pairs of vertices corresponding to each edge
353
+ def edges
354
+ (@vertices + [@vertices.first]).each_cons(2) {|v1,v2| yield v1, v2}
355
+ end
356
+
357
+ def edges_with_index
358
+ index = 0
359
+ (@vertices + [@vertices.first]).each_cons(2) {|v1,v2| yield(Edge.new(v1[:vertex], v2[:vertex]), index); index += 1}
360
+ end
361
+ end
182
362
  end
@@ -48,13 +48,13 @@ also like a {Path} in that it isn't necessarily closed.
48
48
  if @edges.last
49
49
  new_edge = Edge.new(previous, n)
50
50
  if @edges.last.parallel?(new_edge)
51
- @edges.pop # Remove the previous Edge
51
+ popped_edge = @edges.pop # Remove the previous Edge
52
52
  @vertices.pop(@edges.size ? 1 : 2) # Remove the now unused vertex, or vertices
53
- if n == @edges.last.last
54
- @edges.last.last
53
+ if n == popped_edge.first
54
+ popped_edge.first
55
55
  else
56
- push_edge Edge.new(@edges.last.last, n)
57
- push_vertex @edges.last.first
56
+ push_edge Edge.new(popped_edge.first, n)
57
+ push_vertex popped_edge.first
58
58
  push_vertex n
59
59
  n
60
60
  end
@@ -1,6 +1,7 @@
1
1
  require_relative 'cluster_factory'
2
2
  require_relative 'edge'
3
3
  require_relative 'point'
4
+ require_relative 'point_zero'
4
5
  require_relative 'size'
5
6
 
6
7
  module Geometry
@@ -10,13 +11,15 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
10
11
  == Usage
11
12
 
12
13
  === Constructors
13
- rect = Rectangle[[1,2], [2,3]] # Using two corners
14
- rect = Rectangle[[1,2], Size[1,1]] # Using origin and size
15
- rect = Rectangle[1,2,2,3] # Using four sides
14
+ rect = Rectangle.new [1,2], [2,3] # Using two corners
15
+ rect = Rectangle.new from:[1,2], to:[2,3] # Using two corners
16
16
 
17
- rect = Rectangle[10, 20] # origin = [0,0], size = [10, 20]
18
- rect = Rectangle[Size[10, 20]] # origin = [0,0], size = [10, 20]
17
+ rect = Rectangle.new center:[1,2], size:[1,1] # Using a center point and a size
18
+ rect = Rectangle.new origin:[1,2], size:[1,1] # Using an origin point and a size
19
19
 
20
+ rect = Rectangle.new size: [10, 20] # origin = [0,0], size = [10, 20]
21
+ rect = Rectangle.new size: Size[10, 20] # origin = [0,0], size = [10, 20]
22
+ rect = Rectangle.new width: 10, height: 20 # origin = [0,0], size = [10, 20]
20
23
  =end
21
24
 
22
25
  class Rectangle
@@ -44,8 +47,8 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
44
47
  # @return [CenteredRectangle]
45
48
  # @overload new(point0, point1)
46
49
  # Creates a {Rectangle} using the given {Point}s
47
- # @param [Point,Array] point0 A corner
48
- # @param [Point,Array] point1 The other corner
50
+ # @param [Point] point0 A corner
51
+ # @param [Point] point1 The other corner
49
52
  # @overload new(origin, size)
50
53
  # Creates a {Rectangle} from the given origin and size
51
54
  # @param [Point] origin Lower-left corner
@@ -58,21 +61,25 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
58
61
  # @param [Number] right X-coordinate of the right side
59
62
  # @param [Number] top Y-coordinate of the top edge
60
63
  def self.new(*args)
61
- case args.size
62
- when 1
63
- CenteredRectangle.new(args[0])
64
- when 2
65
- if args.all? {|a| a.is_a?(Numeric) }
66
- CenteredRectangle.new(Size[*args])
67
- elsif args.all? {|a| a.is_a?(Array) || a.is_a?(Point) }
68
- original_new(*args)
69
- elsif args[0].is_a?(Point) and args[1].is_a?(Size)
70
- SizedRectangle.new(*args)
71
- end
72
- when 4
73
- raise ArgumentError unless args.all? {|a| a.is_a?(Numeric)}
74
- left, bottom, right, top = *args
75
- original_new(Point[left, bottom], Point[right, top])
64
+ options, args = args.partition {|a| a.is_a? Hash}
65
+ options = options.reduce({}, :merge)
66
+
67
+ if options.has_key?(:size)
68
+ if options.has_key?(:center)
69
+ CenteredRectangle.new(center: options[:center], size: options[:size])
70
+ elsif options.has_key?(:origin)
71
+ SizedRectangle.new(origin: options[:origin], size: options[:size])
72
+ else
73
+ SizedRectangle.new(size: options[:size])
74
+ end
75
+ elsif options.has_key?(:from) and options.has_key?(:to)
76
+ original_new(options[:from], options[:to])
77
+ elsif options.has_key?(:height) and options.has_key?(:width)
78
+ SizedRectangle.new(height: options[:height], width: options[:width])
79
+ elsif (2==args.count) and (args.all? {|a| a.is_a?(Array) || a.is_a?(Point) })
80
+ original_new(*args)
81
+ else
82
+ raise ArgumentError, "Bad Rectangle arguments: #{args}, #{options}"
76
83
  end
77
84
  end
78
85
 
@@ -88,40 +95,63 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
88
95
  if (point0.x > point1.x) && (point0.y > point1.y)
89
96
  point0, point1 = point1, point0
90
97
  else
91
- if point0.x > point1.x
92
- point0.x, point1.x = point1.x, point0.x
93
- end
94
- if point0.y > point1.y
95
- point0.y, point1.y = point1.y, point0.y
96
- end
98
+ p0x, p1x = [point0.x, point1.x].minmax
99
+ p0y, p1y = [point0.y, point1.y].minmax
100
+ point0 = Point[p0x, p0y]
101
+ point1 = Point[p1x, p1y]
97
102
  end
98
103
  @points = [point0, point1]
99
104
  end
100
105
 
106
+ def eql?(other)
107
+ self.points == other.points
108
+ end
109
+ alias :== :eql?
110
+
101
111
  # @group Accessors
102
112
 
113
+ # @return [Rectangle] The smallest axis-aligned {Rectangle} that bounds the receiver
114
+ def bounds
115
+ return Rectangle.new(self.min, self.max)
116
+ end
117
+
103
118
  # @return [Point] The {Rectangle}'s center
104
119
  def center
105
120
  min, max = @points.minmax {|a,b| a.y <=> b.y}
106
- Point[(max.x+min.x)/2.0, (max.y+min.y)/2.0]
121
+ Point[(max.x+min.x)/2, (max.y+min.y)/2]
107
122
  end
108
123
 
109
- # @return [Array<Edge>] The {Rectangle}'s four edges
124
+ # @return [Array<Edge>] The {Rectangle}'s four edges (counterclockwise)
110
125
  def edges
111
126
  point0, point2 = *@points
112
- point1 = Point[point0.x,point2.y]
113
- point3 = Point[point2.x, point0.y]
127
+ point1 = Point[point2.x, point0.y]
128
+ point3 = Point[point0.x, point2.y]
114
129
  [Edge.new(point0, point1),
115
130
  Edge.new(point1, point2),
116
131
  Edge.new(point2, point3),
117
132
  Edge.new(point3, point0)]
118
133
  end
119
134
 
120
- # @return [Array<Point>] The {Rectangle}'s four points (clockwise)
135
+ # @return [Point] The upper right corner of the bounding {Rectangle}
136
+ def max
137
+ @points.last
138
+ end
139
+
140
+ # @return [Point] The lower left corner of the bounding {Rectangle}
141
+ def min
142
+ @points.first
143
+ end
144
+
145
+ # @return [Array<Point>] The lower left and upper right corners of the bounding {Rectangle}
146
+ def minmax
147
+ [self.min, self.max]
148
+ end
149
+
150
+ # @return [Array<Point>] The {Rectangle}'s four points (counterclockwise)
121
151
  def points
122
152
  point0, point2 = *@points
123
- point1 = Point[point0.x,point2.y]
124
- point3 = Point[point2.x, point0.y]
153
+ point1 = Point[point2.x, point0.y]
154
+ point3 = Point[point0.x, point2.y]
125
155
  [point0, point1, point2, point3]
126
156
  end
127
157
 
@@ -146,7 +176,6 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
146
176
  class CenteredRectangle < Rectangle
147
177
  # @return [Point] The {Rectangle}'s center
148
178
  attr_accessor :center
149
- attr_reader :origin
150
179
  # @return [Size] The {Size} of the {Rectangle}
151
180
  attr_accessor :size
152
181
 
@@ -164,17 +193,25 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
164
193
  # @param [Point] center
165
194
  # @param [Size] size
166
195
  def initialize(*args)
167
- if args[0].is_a?(Size)
168
- @center = Point[0,0]
169
- @size = args[0]
170
- elsif args[0].is_a?(Geometry::Point) and args[1].is_a?(Geometry::Size)
171
- @center, @size = args[0,1]
172
- elsif (2 == args.size) and args.all? {|a| a.is_a?(Numeric)}
173
- @center = Point[0,0]
174
- @size = Geometry::Size[*args]
196
+ options, args = args.partition {|a| a.is_a? Hash}
197
+ options = options.reduce({}, :merge)
198
+
199
+ @center = options[:center] ? Point[options[:center]] : PointZero.new
200
+
201
+ if options.has_key?(:size)
202
+ @size = Geometry::Size[options[:size]]
203
+ elsif options.has_key?(:height) and options.has_key?(:width)
204
+ @size = Geometry::Size[options[:width], options[:height]]
205
+ else
206
+ raise ArgumentError, "Bad arguments to CenteredRectangle#new"
175
207
  end
176
208
  end
177
209
 
210
+ def eql?(other)
211
+ (self.center == other.center) && (self.size == other.size)
212
+ end
213
+ alias :== :eql?
214
+
178
215
  # @group Accessors
179
216
  # @return [Array<Edge>] The {Rectangle}'s four edges
180
217
  def edges
@@ -188,6 +225,16 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
188
225
  Edge.new(point3, point0)]
189
226
  end
190
227
 
228
+ # @return [Point] The upper right corner of the bounding {Rectangle}
229
+ def max
230
+ @center + @size/2.0
231
+ end
232
+
233
+ # @return [Point] The lower left corner of the bounding {Rectangle}
234
+ def min
235
+ @center - @size/2.0
236
+ end
237
+
191
238
  # @return [Array<Point>] The {Rectangle}'s four points (clockwise)
192
239
  def points
193
240
  point0 = @center - @size/2.0
@@ -208,8 +255,6 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
208
255
  end
209
256
 
210
257
  class SizedRectangle < Rectangle
211
- # @return [Point] The {Rectangle}'s center
212
- attr_reader :center
213
258
  # @return [Point] The {Rectangle}'s origin
214
259
  attr_accessor :origin
215
260
  # @return [Size] The {Size} of the {Rectangle}
@@ -230,19 +275,27 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
230
275
  # @param [Size] size
231
276
  # @return SizedRectangle
232
277
  def initialize(*args)
233
- if args[0].is_a?(Size)
234
- @origin = Point[0,0]
235
- @size = args[0]
236
- elsif args[0].is_a?(Geometry::Point) and args[1].is_a?(Geometry::Size)
237
- @origin, @size = args[0], args[1]
238
- elsif (2 == args.size) and args.all? {|a| a.is_a?(Numeric)}
239
- @origin = Point[0,0]
240
- @size = Geometry::Size[*args]
278
+ options, args = args.partition {|a| a.is_a? Hash}
279
+ options = options.reduce({}, :merge)
280
+
281
+ @origin = options[:origin] ? Point[options[:origin]] : PointZero.new
282
+
283
+ if options.has_key?(:size)
284
+ @size = Geometry::Size[options[:size]]
285
+ elsif options.has_key?(:height) and options.has_key?(:width)
286
+ @size = Geometry::Size[options[:width], options[:height]]
287
+ else
288
+ raise ArgumentError, "Bad arguments to SizeRectangle#new"
241
289
  end
242
290
  end
243
291
 
292
+ def eql?(other)
293
+ (self.origin == other.origin) && (self.size == other.size)
294
+ end
295
+ alias :== :eql?
244
296
 
245
297
  # @group Accessors
298
+ # @return [Point] The {Rectangle}'s center
246
299
  def center
247
300
  @origin + @size/2
248
301
  end
@@ -259,6 +312,16 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
259
312
  Edge.new(point3, point0)]
260
313
  end
261
314
 
315
+ # @return [Point] The upper right corner of the bounding {Rectangle}
316
+ def max
317
+ @origin + @size
318
+ end
319
+
320
+ # @return [Point] The lower left corner of the bounding {Rectangle}
321
+ def min
322
+ @origin
323
+ end
324
+
262
325
  # @return [Array<Point>] The {Rectangle}'s four points (clockwise)
263
326
  def points
264
327
  point0 = @origin