geometry-in-ruby 0.0.1
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/.gitignore +6 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.markdown +105 -0
- data/Rakefile +24 -0
- data/geometry-in-ruby.gemspec +23 -0
- data/lib/geometry/arc.rb +94 -0
- data/lib/geometry/circle.rb +122 -0
- data/lib/geometry/cluster_factory.rb +15 -0
- data/lib/geometry/edge.rb +140 -0
- data/lib/geometry/line.rb +154 -0
- data/lib/geometry/obround.rb +238 -0
- data/lib/geometry/path.rb +67 -0
- data/lib/geometry/point.rb +163 -0
- data/lib/geometry/point_zero.rb +107 -0
- data/lib/geometry/polygon.rb +368 -0
- data/lib/geometry/polyline.rb +318 -0
- data/lib/geometry/rectangle.rb +378 -0
- data/lib/geometry/regular_polygon.rb +136 -0
- data/lib/geometry/rotation.rb +190 -0
- data/lib/geometry/size.rb +75 -0
- data/lib/geometry/size_zero.rb +70 -0
- data/lib/geometry/square.rb +113 -0
- data/lib/geometry/text.rb +24 -0
- data/lib/geometry/transformation/composition.rb +39 -0
- data/lib/geometry/transformation.rb +171 -0
- data/lib/geometry/triangle.rb +78 -0
- data/lib/geometry/vector.rb +34 -0
- data/lib/geometry.rb +22 -0
- data/test/geometry/arc.rb +25 -0
- data/test/geometry/circle.rb +112 -0
- data/test/geometry/edge.rb +132 -0
- data/test/geometry/line.rb +132 -0
- data/test/geometry/obround.rb +25 -0
- data/test/geometry/path.rb +66 -0
- data/test/geometry/point.rb +258 -0
- data/test/geometry/point_zero.rb +177 -0
- data/test/geometry/polygon.rb +214 -0
- data/test/geometry/polyline.rb +266 -0
- data/test/geometry/rectangle.rb +154 -0
- data/test/geometry/regular_polygon.rb +120 -0
- data/test/geometry/rotation.rb +108 -0
- data/test/geometry/size.rb +97 -0
- data/test/geometry/size_zero.rb +153 -0
- data/test/geometry/square.rb +66 -0
- data/test/geometry/transformation/composition.rb +49 -0
- data/test/geometry/transformation.rb +169 -0
- data/test/geometry/triangle.rb +32 -0
- data/test/geometry/vector.rb +41 -0
- data/test/geometry.rb +5 -0
- metadata +117 -0
@@ -0,0 +1,318 @@
|
|
1
|
+
require_relative 'edge'
|
2
|
+
|
3
|
+
module Geometry
|
4
|
+
|
5
|
+
=begin rdoc
|
6
|
+
A {Polyline} is like a {Polygon} in that it only contains straight lines, but
|
7
|
+
also like a {Path} in that it isn't necessarily closed.
|
8
|
+
|
9
|
+
{http://en.wikipedia.org/wiki/Polyline}
|
10
|
+
|
11
|
+
== Usage
|
12
|
+
|
13
|
+
=end
|
14
|
+
|
15
|
+
class Polyline
|
16
|
+
attr_reader :edges, :vertices
|
17
|
+
|
18
|
+
# Construct a new Polyline from Points and/or Edges
|
19
|
+
# @note The constructor will try to convert all of its arguments into {Point}s and
|
20
|
+
# {Edge}s. Then successive {Point}s will be collpased into {Edge}s. Successive
|
21
|
+
# {Edge}s that share a common vertex will be added to the new {Polyline}. If
|
22
|
+
# there's a gap between {Edge}s it will be automatically filled with a new
|
23
|
+
# {Edge}.
|
24
|
+
# @overload initialize(Edge, Edge, ...)
|
25
|
+
# @return [Polyline]
|
26
|
+
# @overload initialize(Point, Point, ...)
|
27
|
+
# @return [Polyline]
|
28
|
+
def initialize(*args)
|
29
|
+
args.map! {|a| (a.is_a?(Array) || a.is_a?(Vector)) ? Point[a] : a}
|
30
|
+
args.each {|a| raise ArgumentError, "Unknown argument type #{a.class}" unless a.is_a?(Point) or a.is_a?(Edge) }
|
31
|
+
|
32
|
+
@edges = [];
|
33
|
+
@vertices = [];
|
34
|
+
|
35
|
+
first = args.shift
|
36
|
+
if first.is_a?(Point)
|
37
|
+
@vertices.push first
|
38
|
+
elsif first.is_a?(Edge)
|
39
|
+
@edges.push first
|
40
|
+
@vertices.push *(first.to_a)
|
41
|
+
end
|
42
|
+
|
43
|
+
args.reduce(@vertices.last) do |previous,n|
|
44
|
+
if n.is_a?(Point)
|
45
|
+
if n == previous # Ignore repeated Points
|
46
|
+
previous
|
47
|
+
else
|
48
|
+
if @edges.last
|
49
|
+
new_edge = Edge.new(previous, n)
|
50
|
+
if @edges.last.parallel?(new_edge)
|
51
|
+
popped_edge = @edges.pop # Remove the previous Edge
|
52
|
+
@vertices.pop(@edges.size ? 1 : 2) # Remove the now unused vertex, or vertices
|
53
|
+
if n == popped_edge.first
|
54
|
+
popped_edge.first
|
55
|
+
else
|
56
|
+
push_edge Edge.new(popped_edge.first, n)
|
57
|
+
push_vertex popped_edge.first
|
58
|
+
push_vertex n
|
59
|
+
n
|
60
|
+
end
|
61
|
+
else
|
62
|
+
push_edge Edge.new(previous, n)
|
63
|
+
push_vertex n
|
64
|
+
n
|
65
|
+
end
|
66
|
+
else
|
67
|
+
push_edge Edge.new(previous, n)
|
68
|
+
push_vertex n
|
69
|
+
n
|
70
|
+
end
|
71
|
+
end
|
72
|
+
elsif n.is_a?(Edge)
|
73
|
+
if previous == n.first
|
74
|
+
push_edge n
|
75
|
+
push_vertex n.last
|
76
|
+
elsif previous == n.last
|
77
|
+
push_edge n.reverse!
|
78
|
+
push_vertex n.last
|
79
|
+
else
|
80
|
+
e = Edge.new(previous, n.first)
|
81
|
+
push_edge e, n
|
82
|
+
push_vertex *(e.to_a), *(n.to_a)
|
83
|
+
end
|
84
|
+
n.last
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Check the equality of two {Polyline}s. Note that if two {Polyline}s have
|
90
|
+
# opposite winding, but are otherwise identical, they will be considered unequal.
|
91
|
+
# @return [Bool] true if both {Polyline}s have equal edges
|
92
|
+
def eql?(other)
|
93
|
+
@vertices.zip(other.vertices).all? {|a,b| a == b}
|
94
|
+
end
|
95
|
+
alias :== :eql?
|
96
|
+
|
97
|
+
# Clone the receiver, close it, then return it
|
98
|
+
# @return [Polyline] the closed clone of the receiver
|
99
|
+
def close
|
100
|
+
clone.close!
|
101
|
+
end
|
102
|
+
|
103
|
+
# Close the receiver and return it
|
104
|
+
# @return [Polyline] the receiver after closing
|
105
|
+
def close!
|
106
|
+
push_edge Edge.new(@edges.last.last, @edges.first.first) unless @edges.empty? || closed?
|
107
|
+
self
|
108
|
+
end
|
109
|
+
|
110
|
+
# Check to see if the {Polyline} is closed (ie. is it a {Polygon}?)
|
111
|
+
# @return [Bool] true if the {Polyline} is closed (the first vertex is equal to the last vertex)
|
112
|
+
def closed?
|
113
|
+
@edges.last.last == @edges.first.first
|
114
|
+
end
|
115
|
+
|
116
|
+
# Clone the receiver, reverse it, then return it
|
117
|
+
# @return [Polyline] the reversed clone
|
118
|
+
def reverse
|
119
|
+
self.class.new *(edges.reverse.map! {|edge| edge.reverse! })
|
120
|
+
end
|
121
|
+
|
122
|
+
# Reverse the receiver and return it
|
123
|
+
# @return [Polyline] the reversed receiver
|
124
|
+
def reverse!
|
125
|
+
vertices.reverse!
|
126
|
+
edges.reverse!.map! {|edge| edge.reverse! }
|
127
|
+
self
|
128
|
+
end
|
129
|
+
|
130
|
+
# @group Bisectors
|
131
|
+
|
132
|
+
# Generate the angle bisector unit vectors for each vertex
|
133
|
+
# @note If the {Polyline} isn't closed (the normal case), then the first and
|
134
|
+
# last vertices will be given bisectors that are perpendicular to themselves.
|
135
|
+
# @return [Array<Vector>] the unit {Vector}s representing the angle bisector of each vertex
|
136
|
+
def bisectors
|
137
|
+
# Multiplying each bisector by the sign of k flips any bisectors that aren't pointing towards the interior of the angle
|
138
|
+
bisector_map {|b, k| k <=> 0 }
|
139
|
+
end
|
140
|
+
|
141
|
+
# Generate left-side angle bisector unit vectors for each vertex
|
142
|
+
# @note This is similar to the #bisector method, but generates vectors that always point to the left side of the {Polyline} instead of towards the inside of each corner
|
143
|
+
# @return [Array<Vector>] the unit {Vector}s representing the left-side angle bisector of each vertex
|
144
|
+
def left_bisectors
|
145
|
+
bisector_map
|
146
|
+
end
|
147
|
+
|
148
|
+
# Generate right-side angle bisector unit vectors for each vertex
|
149
|
+
# @note This is similar to the #bisector method, but generates vectors that always point to the right side of the {Polyline} instead of towards the inside of each corner
|
150
|
+
# @return [Array<Vector>] the unit {Vector}s representing the ride-side angle bisector of each vertex
|
151
|
+
def right_bisectors
|
152
|
+
bisector_map {|b, k| -1 }
|
153
|
+
end
|
154
|
+
|
155
|
+
# Generate the spokes for each vertex. A spoke is the same as a bisector, but in the oppostire direction (bisectors point towards the inside of each corner; spokes point towards the outside)
|
156
|
+
# @note If the {Polyline} isn't closed (the normal case), then the first and
|
157
|
+
# last vertices will be given bisectors that are perpendicular to themselves.
|
158
|
+
# @return [Array<Vector>] the unit {Vector}s representing the spoke of each vertex
|
159
|
+
def spokes
|
160
|
+
# Multiplying each bisector by the negated sign of k flips any bisectors that aren't pointing towards the exterior of the angle
|
161
|
+
bisector_map {|b, k| 0 <=> k }
|
162
|
+
end
|
163
|
+
|
164
|
+
# @endgroup Bisectors
|
165
|
+
|
166
|
+
# Offset the receiver by the specified distance
|
167
|
+
# @note A positive distance will offset to the left, and a negative distance to the right.
|
168
|
+
# @param [Number] distance The distance to offset by
|
169
|
+
# @return [Polyline] A new {Polyline} outset by the given distance
|
170
|
+
def offset(distance)
|
171
|
+
bisector_pairs = if closed?
|
172
|
+
bisector_edges = offset_bisectors(distance)
|
173
|
+
bisector_edges.push(bisector_edges.first).each_cons(2)
|
174
|
+
else
|
175
|
+
offset_bisectors(distance).each_cons(2)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Create the offset edges and then wrap them in Hashes so the edges
|
179
|
+
# can be altered while walking the array
|
180
|
+
active_edges = edges.zip(bisector_pairs).map do |e,offset|
|
181
|
+
offset_edge = Edge.new(e.first+offset.first.vector, e.last+offset.last.vector)
|
182
|
+
|
183
|
+
# Skip zero-length edges
|
184
|
+
{:edge => (offset_edge.first == offset_edge.last) ? nil : offset_edge}
|
185
|
+
end
|
186
|
+
|
187
|
+
# Walk the array and handle any intersections
|
188
|
+
for i in 0..(active_edges.count-1) do
|
189
|
+
e1 = active_edges[i][:edge]
|
190
|
+
next unless e1 # Ignore deleted edges
|
191
|
+
|
192
|
+
intersection, j = find_last_intersection(active_edges, i, e1)
|
193
|
+
if intersection
|
194
|
+
e2 = active_edges[j][:edge]
|
195
|
+
if intersection.is_a? Point
|
196
|
+
active_edges[i][:edge] = Edge.new(e1.first, intersection)
|
197
|
+
active_edges[j][:edge] = Edge.new(intersection, e2.last)
|
198
|
+
else
|
199
|
+
# Handle the collinear case
|
200
|
+
active_edges[i][:edge] = Edge.new(e1.first, e2.last)
|
201
|
+
active_edges[j].delete(:edge)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Delete everything between e1 and e2
|
205
|
+
for k in i..j do
|
206
|
+
next if (k==i) or (k==j) # Exclude e1 and e2
|
207
|
+
active_edges[k].delete(:edge)
|
208
|
+
end
|
209
|
+
|
210
|
+
redo # Recheck the modified edges
|
211
|
+
end
|
212
|
+
end
|
213
|
+
Polyline.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten)
|
214
|
+
end
|
215
|
+
alias :leftset :offset
|
216
|
+
|
217
|
+
# Rightset the receiver by the specified distance
|
218
|
+
# @param [Number] distance The distance to offset by
|
219
|
+
# @return [Polyline] A new {Polyline} rightset by the given distance
|
220
|
+
def rightset(distance)
|
221
|
+
offset(-distance)
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
# Generate bisectors and k values with an optional mapping block
|
227
|
+
# @note If the {Polyline} isn't closed (the normal case), then the first and
|
228
|
+
# last vertices will be given bisectors that are perpendicular to themselves.
|
229
|
+
# @return [Array<Vector>] the unit {Vector}s representing the angle bisector of each vertex
|
230
|
+
def bisector_map
|
231
|
+
winding = 0
|
232
|
+
tangent_loop.each_cons(2).map do |v1,v2|
|
233
|
+
k = v1[0]*v2[1] - v1[1]*v2[0] # z-component of v1 x v2
|
234
|
+
winding += k
|
235
|
+
if v1 == v2 # collinear, same direction?
|
236
|
+
bisector = Vector[-v1[1], v1[0]]
|
237
|
+
block_given? ? (bisector * yield(bisector, 1)) : bisector
|
238
|
+
elsif 0 == k # collinear, reverse direction
|
239
|
+
nil
|
240
|
+
else
|
241
|
+
bisector_y = (v2[1] - v1[1])/k
|
242
|
+
|
243
|
+
# If v1 or v2 happens to be horizontal, then the other one must be used when calculating
|
244
|
+
# the x-component of the bisector (to avoid a divide by zero). But, comparing floats
|
245
|
+
# with zero is problematic, so use the one with the largest y-component instead checking
|
246
|
+
# for a y-component equal to zero.
|
247
|
+
v = (v2[1].abs > v1[1].abs) ? v2 : v1
|
248
|
+
|
249
|
+
bisector = Vector[(v[0]*bisector_y - 1)/v[1], bisector_y]
|
250
|
+
block_given? ? (bisector * yield(bisector, k)) : bisector
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# @group Helpers for offset()
|
256
|
+
|
257
|
+
# Vertex bisectors suitable for offsetting
|
258
|
+
# @param [Number] length The distance to offset by. Positive generates left offset bisectors, negative generates right offset bisectors
|
259
|
+
# @return [Array<Edge>] {Edge}s representing the bisectors
|
260
|
+
def offset_bisectors(length)
|
261
|
+
vertices.zip(left_bisectors).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil}
|
262
|
+
end
|
263
|
+
|
264
|
+
# Generate the tangents and fake a circular buffer while accounting for closedness
|
265
|
+
# @return [Array<Vector>] the tangents
|
266
|
+
def tangent_loop
|
267
|
+
edges.map {|e| e.direction }.tap do |tangents|
|
268
|
+
# Generating a bisector for each vertex requires an edge on both sides of each vertex.
|
269
|
+
# Obviously, the first and last vertices each have only a single adjacent edge, unless the
|
270
|
+
# Polyline happens to be closed (like a Polygon). When not closed, duplicate the
|
271
|
+
# first and last direction vectors to fake the adjacent edges. This causes the first and last
|
272
|
+
# edges to have bisectors that are perpendicular to themselves.
|
273
|
+
if closed?
|
274
|
+
# Prepend the last direction vector so that the last edge can be used to find the bisector for the first vertex
|
275
|
+
tangents.unshift tangents.last
|
276
|
+
else
|
277
|
+
# Duplicate the first and last direction vectors to compensate for not having edges adjacent to the first and last vertices
|
278
|
+
tangents.unshift(tangents.first)
|
279
|
+
tangents.push(tangents.last)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Find the next edge that intersects with e, starting at index i
|
285
|
+
def find_next_intersection(edges, i, e)
|
286
|
+
for j in i..(edges.count-1)
|
287
|
+
e2 = edges[j][:edge]
|
288
|
+
next if !e2 || e.connected?(e2)
|
289
|
+
intersection = e.intersection(e2)
|
290
|
+
return [intersection, j] if intersection
|
291
|
+
end
|
292
|
+
nil
|
293
|
+
end
|
294
|
+
|
295
|
+
# Find the last edge that intersects with e, starting at index i
|
296
|
+
def find_last_intersection(edges, i, e)
|
297
|
+
intersection, intersection_at = nil, nil
|
298
|
+
for j in i..(edges.count-1)
|
299
|
+
e2 = edges[j][:edge]
|
300
|
+
next if !e2 || e.connected?(e2)
|
301
|
+
_intersection = e.intersection(e2)
|
302
|
+
intersection, intersection_at = _intersection, j if _intersection
|
303
|
+
end
|
304
|
+
[intersection, intersection_at]
|
305
|
+
end
|
306
|
+
# @endgroup
|
307
|
+
|
308
|
+
def push_edge(*e)
|
309
|
+
@edges.push *e
|
310
|
+
@edges.uniq!
|
311
|
+
end
|
312
|
+
|
313
|
+
def push_vertex(*v)
|
314
|
+
@vertices.push *v
|
315
|
+
@vertices.uniq!
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
@@ -0,0 +1,378 @@
|
|
1
|
+
require_relative 'cluster_factory'
|
2
|
+
require_relative 'edge'
|
3
|
+
require_relative 'point'
|
4
|
+
require_relative 'point_zero'
|
5
|
+
require_relative 'size'
|
6
|
+
|
7
|
+
module Geometry
|
8
|
+
=begin
|
9
|
+
The {Rectangle} class cluster represents your typical arrangement of 4 corners and 4 sides.
|
10
|
+
|
11
|
+
== Usage
|
12
|
+
|
13
|
+
=== Constructors
|
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
|
+
|
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
|
+
|
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]
|
23
|
+
=end
|
24
|
+
|
25
|
+
class Rectangle
|
26
|
+
include ClusterFactory
|
27
|
+
|
28
|
+
# @return [Point] The {Rectangle}'s center
|
29
|
+
attr_reader :center
|
30
|
+
# @return [Number] Height of the {Rectangle}
|
31
|
+
attr_reader :height
|
32
|
+
# @return [Point] The {Rectangle}'s origin
|
33
|
+
attr_reader :origin
|
34
|
+
# @return [Size] The {Size} of the {Rectangle}
|
35
|
+
attr_reader :size
|
36
|
+
# @return [Number] Width of the {Rectangle}
|
37
|
+
attr_reader :width
|
38
|
+
|
39
|
+
# @overload new(width, height)
|
40
|
+
# Creates a {Rectangle} of the given width and height, centered on the origin
|
41
|
+
# @param [Number] height Height
|
42
|
+
# @param [Number] width Width
|
43
|
+
# @return [CenteredRectangle]
|
44
|
+
# @overload new(size)
|
45
|
+
# Creates a {Rectangle} of the given {Size} centered on the origin
|
46
|
+
# @param [Size] size Width and height
|
47
|
+
# @return [CenteredRectangle]
|
48
|
+
# @overload new(point0, point1)
|
49
|
+
# Creates a {Rectangle} using the given {Point}s
|
50
|
+
# @param [Point] point0 A corner
|
51
|
+
# @param [Point] point1 The other corner
|
52
|
+
# @overload new(origin, size)
|
53
|
+
# Creates a {Rectangle} from the given origin and size
|
54
|
+
# @param [Point] origin Lower-left corner
|
55
|
+
# @param [Size] size Width and height
|
56
|
+
# @return [SizedRectangle]
|
57
|
+
# @overload new(left, bottom, right, top)
|
58
|
+
# Creates a {Rectangle} from the locations of each side
|
59
|
+
# @param [Number] left X-coordinate of the left side
|
60
|
+
# @param [Number] bottom Y-coordinate of the bottom edge
|
61
|
+
# @param [Number] right X-coordinate of the right side
|
62
|
+
# @param [Number] top Y-coordinate of the top edge
|
63
|
+
def self.new(*args)
|
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
|
+
elsif options.empty?
|
82
|
+
raise ArgumentError, "#{self} arguments must be named, not: #{args}"
|
83
|
+
else
|
84
|
+
raise ArgumentError, "Bad Rectangle arguments: #{args}, #{options}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Creates a {Rectangle} using the given {Point}s
|
89
|
+
# @param [Point] point0 A corner (ie. bottom-left)
|
90
|
+
# @param [Point] point1 The other corner (ie. top-right)
|
91
|
+
def initialize(point0, point1)
|
92
|
+
point0 = Point[point0]
|
93
|
+
point1 = Point[point1]
|
94
|
+
raise(ArgumentError, "Point sizes must match") unless point0.size == point1.size
|
95
|
+
|
96
|
+
# Reorder the points to get lower-left and upper-right
|
97
|
+
if (point0.x > point1.x) && (point0.y > point1.y)
|
98
|
+
point0, point1 = point1, point0
|
99
|
+
else
|
100
|
+
p0x, p1x = [point0.x, point1.x].minmax
|
101
|
+
p0y, p1y = [point0.y, point1.y].minmax
|
102
|
+
point0 = Point[p0x, p0y]
|
103
|
+
point1 = Point[p1x, p1y]
|
104
|
+
end
|
105
|
+
@points = [point0, point1]
|
106
|
+
end
|
107
|
+
|
108
|
+
def eql?(other)
|
109
|
+
self.points == other.points
|
110
|
+
end
|
111
|
+
alias :== :eql?
|
112
|
+
|
113
|
+
# @group Accessors
|
114
|
+
|
115
|
+
# @return [Rectangle] The smallest axis-aligned {Rectangle} that bounds the receiver
|
116
|
+
def bounds
|
117
|
+
return Rectangle.new(self.min, self.max)
|
118
|
+
end
|
119
|
+
|
120
|
+
# @return [Point] The {Rectangle}'s center
|
121
|
+
def center
|
122
|
+
min, max = @points.minmax {|a,b| a.y <=> b.y}
|
123
|
+
Point[(max.x+min.x)/2, (max.y+min.y)/2]
|
124
|
+
end
|
125
|
+
|
126
|
+
# @return [Array<Edge>] The {Rectangle}'s four edges (counterclockwise)
|
127
|
+
def edges
|
128
|
+
point0, point2 = *@points
|
129
|
+
point1 = Point[point2.x, point0.y]
|
130
|
+
point3 = Point[point0.x, point2.y]
|
131
|
+
[Edge.new(point0, point1),
|
132
|
+
Edge.new(point1, point2),
|
133
|
+
Edge.new(point2, point3),
|
134
|
+
Edge.new(point3, point0)]
|
135
|
+
end
|
136
|
+
|
137
|
+
# @return [Point] The upper right corner of the bounding {Rectangle}
|
138
|
+
def max
|
139
|
+
@points.last
|
140
|
+
end
|
141
|
+
|
142
|
+
# @return [Point] The lower left corner of the bounding {Rectangle}
|
143
|
+
def min
|
144
|
+
@points.first
|
145
|
+
end
|
146
|
+
|
147
|
+
# @return [Array<Point>] The lower left and upper right corners of the bounding {Rectangle}
|
148
|
+
def minmax
|
149
|
+
[self.min, self.max]
|
150
|
+
end
|
151
|
+
|
152
|
+
# @return [Array<Point>] The {Rectangle}'s four points (counterclockwise)
|
153
|
+
def points
|
154
|
+
point0, point2 = *@points
|
155
|
+
point1 = Point[point2.x, point0.y]
|
156
|
+
point3 = Point[point0.x, point2.y]
|
157
|
+
[point0, point1, point2, point3]
|
158
|
+
end
|
159
|
+
|
160
|
+
def origin
|
161
|
+
minx = @points.min {|a,b| a.x <=> b.x}
|
162
|
+
miny = @points.min {|a,b| a.y <=> b.y}
|
163
|
+
Point[minx.x, miny.y]
|
164
|
+
end
|
165
|
+
|
166
|
+
def height
|
167
|
+
min, max = @points.minmax {|a,b| a.y <=> b.y}
|
168
|
+
max.y - min.y
|
169
|
+
end
|
170
|
+
|
171
|
+
def width
|
172
|
+
min, max = @points.minmax {|a,b| a.x <=> b.x}
|
173
|
+
max.x - min.x
|
174
|
+
end
|
175
|
+
# @endgroup
|
176
|
+
|
177
|
+
# Create a new {Rectangle} from the receiver that's inset by the given amount
|
178
|
+
# @overload inset(x, y)
|
179
|
+
# @overload inset(top, left, bottom, right)
|
180
|
+
# @overload inset(x, y)
|
181
|
+
# @option options [Number] :x Inset from the left and right sides
|
182
|
+
# @option options [Number] :y Inset from the top and bottom
|
183
|
+
# @overload inset(top, left, bottom, right)
|
184
|
+
# @option options [Number] :bottom The inset from the bottom of the {Rectangle}
|
185
|
+
# @option options [Number] :left The inset from the left side of the {Rectangle}
|
186
|
+
# @option options [Number] :right The inset from the right side of the {Rectangle}
|
187
|
+
# @option options [Number] :top The inset from the top of the {Rectangle}
|
188
|
+
def inset(*args)
|
189
|
+
options, args = args.partition {|a| a.is_a? Hash}
|
190
|
+
options = options.reduce({}, :merge)
|
191
|
+
raise ArumentError, "Can't specify both arguments and options" if !args.empty? && !options.empty?
|
192
|
+
|
193
|
+
if 1 == args.size
|
194
|
+
distance = args.shift
|
195
|
+
Rectangle.new from:(min + distance), to:(max - distance)
|
196
|
+
elsif 2 == args.size
|
197
|
+
distance = Point[*args]
|
198
|
+
Rectangle.new from:(min + distance), to:(max - distance)
|
199
|
+
elsif 4 == args.size
|
200
|
+
top, left, bottom, right = *args
|
201
|
+
Rectangle.new from:(min + Point[left, bottom]), to:(max - Point[right, top])
|
202
|
+
elsif options[:x] && options[:y]
|
203
|
+
distance = Point[options[:x], options[:y]]
|
204
|
+
Rectangle.new from:(min + distance), to:(max - distance)
|
205
|
+
elsif options[:top] && options[:left] && options[:bottom] && options[:right]
|
206
|
+
Rectangle.new from:(min + Point[options[:left], options[:bottom]]), to:(max - Point[options[:right], options[:top]])
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
class CenteredRectangle < Rectangle
|
212
|
+
# @return [Point] The {Rectangle}'s center
|
213
|
+
attr_accessor :center
|
214
|
+
# @return [Size] The {Size} of the {Rectangle}
|
215
|
+
attr_accessor :size
|
216
|
+
|
217
|
+
# @overload new(width, height)
|
218
|
+
# Creates a {Rectangle} of the given width and height, centered on the origin
|
219
|
+
# @param [Number] height Height
|
220
|
+
# @param [Number] width Width
|
221
|
+
# @return [CenteredRectangle]
|
222
|
+
# @overload new(size)
|
223
|
+
# Creates a {Rectangle} of the given {Size} centered on the origin
|
224
|
+
# @param [Size] size Width and height
|
225
|
+
# @return [CenteredRectangle]
|
226
|
+
# @overload new(center, size)
|
227
|
+
# Creates a {Rectangle} with the given center point and size
|
228
|
+
# @param [Point] center
|
229
|
+
# @param [Size] size
|
230
|
+
def initialize(*args)
|
231
|
+
options, args = args.partition {|a| a.is_a? Hash}
|
232
|
+
options = options.reduce({}, :merge)
|
233
|
+
|
234
|
+
@center = options[:center] ? Point[options[:center]] : PointZero.new
|
235
|
+
|
236
|
+
if options.has_key?(:size)
|
237
|
+
@size = Geometry::Size[options[:size]]
|
238
|
+
elsif options.has_key?(:height) and options.has_key?(:width)
|
239
|
+
@size = Geometry::Size[options[:width], options[:height]]
|
240
|
+
else
|
241
|
+
raise ArgumentError, "Bad arguments to CenteredRectangle#new"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def eql?(other)
|
246
|
+
(self.center == other.center) && (self.size == other.size)
|
247
|
+
end
|
248
|
+
alias :== :eql?
|
249
|
+
|
250
|
+
# @group Accessors
|
251
|
+
# @return [Array<Edge>] The {Rectangle}'s four edges
|
252
|
+
def edges
|
253
|
+
point0 = @center - @size/2.0
|
254
|
+
point2 = @center + @size/2.0
|
255
|
+
point1 = Point[point0.x,point2.y]
|
256
|
+
point3 = Point[point2.x, point0.y]
|
257
|
+
[Edge.new(point0, point1),
|
258
|
+
Edge.new(point1, point2),
|
259
|
+
Edge.new(point2, point3),
|
260
|
+
Edge.new(point3, point0)]
|
261
|
+
end
|
262
|
+
|
263
|
+
# @return [Point] The upper right corner of the bounding {Rectangle}
|
264
|
+
def max
|
265
|
+
@center + @size/2.0
|
266
|
+
end
|
267
|
+
|
268
|
+
# @return [Point] The lower left corner of the bounding {Rectangle}
|
269
|
+
def min
|
270
|
+
@center - @size/2.0
|
271
|
+
end
|
272
|
+
|
273
|
+
# @return [Array<Point>] The {Rectangle}'s four points (clockwise)
|
274
|
+
def points
|
275
|
+
point0 = @center - @size/2.0
|
276
|
+
point2 = @center + @size/2.0
|
277
|
+
point1 = Point[point0.x,point2.y]
|
278
|
+
point3 = Point[point2.x, point0.y]
|
279
|
+
[point0, point1, point2, point3]
|
280
|
+
end
|
281
|
+
|
282
|
+
def height
|
283
|
+
@size.height
|
284
|
+
end
|
285
|
+
|
286
|
+
def width
|
287
|
+
@size.width
|
288
|
+
end
|
289
|
+
# @endgroup
|
290
|
+
end
|
291
|
+
|
292
|
+
class SizedRectangle < Rectangle
|
293
|
+
# @return [Point] The {Rectangle}'s origin
|
294
|
+
attr_accessor :origin
|
295
|
+
# @return [Size] The {Size} of the {Rectangle}
|
296
|
+
attr_accessor :size
|
297
|
+
|
298
|
+
# @overload new(width, height)
|
299
|
+
# Creates a {Rectangle} of the given width and height with its origin at [0,0]
|
300
|
+
# @param [Number] height Height
|
301
|
+
# @param [Number] width Width
|
302
|
+
# @return SizedRectangle
|
303
|
+
# @overload new(size)
|
304
|
+
# Creates a {Rectangle} of the given {Size} with its origin at [0,0]
|
305
|
+
# @param [Size] size Width and height
|
306
|
+
# @return SizedRectangle
|
307
|
+
# @overload new(origin, size)
|
308
|
+
# Creates a {Rectangle} with the given origin point and size
|
309
|
+
# @param [Point] origin
|
310
|
+
# @param [Size] size
|
311
|
+
# @return SizedRectangle
|
312
|
+
def initialize(*args)
|
313
|
+
options, args = args.partition {|a| a.is_a? Hash}
|
314
|
+
options = options.reduce({}, :merge)
|
315
|
+
|
316
|
+
@origin = options[:origin] ? Point[options[:origin]] : PointZero.new
|
317
|
+
|
318
|
+
if options.has_key?(:size)
|
319
|
+
@size = Geometry::Size[options[:size]]
|
320
|
+
elsif options.has_key?(:height) and options.has_key?(:width)
|
321
|
+
@size = Geometry::Size[options[:width], options[:height]]
|
322
|
+
else
|
323
|
+
raise ArgumentError, "Bad arguments to SizeRectangle#new"
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def eql?(other)
|
328
|
+
(self.origin == other.origin) && (self.size == other.size)
|
329
|
+
end
|
330
|
+
alias :== :eql?
|
331
|
+
|
332
|
+
# @group Accessors
|
333
|
+
# @return [Point] The {Rectangle}'s center
|
334
|
+
def center
|
335
|
+
@origin + @size/2
|
336
|
+
end
|
337
|
+
|
338
|
+
# @return [Array<Edge>] The {Rectangle}'s four edges
|
339
|
+
def edges
|
340
|
+
point0 = @origin
|
341
|
+
point2 = @origin + @size
|
342
|
+
point1 = Point[point0.x,point2.y]
|
343
|
+
point3 = Point[point2.x, point0.y]
|
344
|
+
[Edge.new(point0, point1),
|
345
|
+
Edge.new(point1, point2),
|
346
|
+
Edge.new(point2, point3),
|
347
|
+
Edge.new(point3, point0)]
|
348
|
+
end
|
349
|
+
|
350
|
+
# @return [Point] The upper right corner of the bounding {Rectangle}
|
351
|
+
def max
|
352
|
+
@origin + @size
|
353
|
+
end
|
354
|
+
|
355
|
+
# @return [Point] The lower left corner of the bounding {Rectangle}
|
356
|
+
def min
|
357
|
+
@origin
|
358
|
+
end
|
359
|
+
|
360
|
+
# @return [Array<Point>] The {Rectangle}'s four points (clockwise)
|
361
|
+
def points
|
362
|
+
point0 = @origin
|
363
|
+
point2 = @origin + @size
|
364
|
+
point1 = Point[point0.x,point2.y]
|
365
|
+
point3 = Point[point2.x, point0.y]
|
366
|
+
[point0, point1, point2, point3]
|
367
|
+
end
|
368
|
+
|
369
|
+
def height
|
370
|
+
@size.height
|
371
|
+
end
|
372
|
+
|
373
|
+
def width
|
374
|
+
@size.width
|
375
|
+
end
|
376
|
+
# @endgroup
|
377
|
+
end
|
378
|
+
end
|