geometry 5 → 6
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -1
- data/README.markdown +26 -5
- data/Rakefile +3 -0
- data/geometry.gemspec +1 -1
- data/lib/geometry.rb +2 -33
- data/lib/geometry/arc.rb +21 -17
- data/lib/geometry/circle.rb +40 -12
- data/lib/geometry/edge.rb +22 -0
- data/lib/geometry/line.rb +21 -0
- data/lib/geometry/obround.rb +238 -0
- data/lib/geometry/path.rb +22 -10
- data/lib/geometry/point.rb +21 -9
- data/lib/geometry/point_zero.rb +39 -2
- data/lib/geometry/polygon.rb +180 -0
- data/lib/geometry/polyline.rb +5 -5
- data/lib/geometry/rectangle.rb +117 -54
- data/lib/geometry/regular_polygon.rb +104 -0
- data/lib/geometry/rotation.rb +5 -0
- data/lib/geometry/square.rb +22 -12
- data/lib/geometry/transformation.rb +12 -0
- data/lib/geometry/vector.rb +4 -0
- data/test/geometry.rb +0 -10
- data/test/geometry/arc.rb +18 -15
- data/test/geometry/circle.rb +55 -2
- data/test/geometry/edge.rb +31 -0
- data/test/geometry/line.rb +25 -0
- data/test/geometry/obround.rb +25 -0
- data/test/geometry/path.rb +4 -5
- data/test/geometry/point.rb +12 -12
- data/test/geometry/point_zero.rb +29 -3
- data/test/geometry/polygon.rb +61 -1
- data/test/geometry/polyline.rb +0 -1
- data/test/geometry/rectangle.rb +87 -32
- data/test/geometry/regular_polygon.rb +84 -0
- data/test/geometry/rotation.rb +8 -0
- data/test/geometry/size_zero.rb +2 -0
- data/test/geometry/square.rb +15 -5
- data/test/geometry/transformation.rb +13 -0
- data/test/geometry/vector.rb +18 -0
- metadata +15 -3
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
|
-
|
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
|
28
|
-
when Arc, Edge then
|
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
|
-
|
30
|
+
last
|
31
31
|
when Edge
|
32
32
|
case previous
|
33
|
-
when Point then
|
34
|
-
when Arc, Edge then
|
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
|
-
|
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
|
-
|
43
|
+
push Edge.new(previous, n.first)
|
44
44
|
end
|
45
45
|
when Arc, Edge
|
46
|
-
|
46
|
+
push Edge.new(previous.last, n.first) unless previous.last == n.first
|
47
47
|
end
|
48
|
-
|
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
|
data/lib/geometry/point.rb
CHANGED
@@ -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?
|
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
|
-
|
44
|
-
|
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
|
-
|
50
|
-
|
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
|
-
|
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
|
-
|
131
|
-
Point[@elements.first - other]
|
143
|
+
Point[@elements.map {|e| e - other}]
|
132
144
|
when PointZero
|
133
145
|
self
|
134
146
|
else
|
data/lib/geometry/point_zero.rb
CHANGED
@@ -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.
|
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 }
|
data/lib/geometry/polygon.rb
CHANGED
@@ -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
|
data/lib/geometry/polyline.rb
CHANGED
@@ -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
|
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 ==
|
54
|
-
|
53
|
+
if n == popped_edge.first
|
54
|
+
popped_edge.first
|
55
55
|
else
|
56
|
-
push_edge Edge.new(
|
57
|
-
push_vertex
|
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
|
data/lib/geometry/rectangle.rb
CHANGED
@@ -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[
|
14
|
-
rect = Rectangle[
|
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[
|
18
|
-
rect = Rectangle[
|
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
|
48
|
-
# @param [Point
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
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[
|
113
|
-
point3 = Point[
|
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 [
|
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[
|
124
|
-
point3 = Point[
|
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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
@
|
174
|
-
|
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
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
@
|
240
|
-
|
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
|