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