geometry 5 → 6

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