geometry-in-ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/.gitignore +6 -0
  2. data/Gemfile +7 -0
  3. data/LICENSE +21 -0
  4. data/README.markdown +105 -0
  5. data/Rakefile +24 -0
  6. data/geometry-in-ruby.gemspec +23 -0
  7. data/lib/geometry/arc.rb +94 -0
  8. data/lib/geometry/circle.rb +122 -0
  9. data/lib/geometry/cluster_factory.rb +15 -0
  10. data/lib/geometry/edge.rb +140 -0
  11. data/lib/geometry/line.rb +154 -0
  12. data/lib/geometry/obround.rb +238 -0
  13. data/lib/geometry/path.rb +67 -0
  14. data/lib/geometry/point.rb +163 -0
  15. data/lib/geometry/point_zero.rb +107 -0
  16. data/lib/geometry/polygon.rb +368 -0
  17. data/lib/geometry/polyline.rb +318 -0
  18. data/lib/geometry/rectangle.rb +378 -0
  19. data/lib/geometry/regular_polygon.rb +136 -0
  20. data/lib/geometry/rotation.rb +190 -0
  21. data/lib/geometry/size.rb +75 -0
  22. data/lib/geometry/size_zero.rb +70 -0
  23. data/lib/geometry/square.rb +113 -0
  24. data/lib/geometry/text.rb +24 -0
  25. data/lib/geometry/transformation/composition.rb +39 -0
  26. data/lib/geometry/transformation.rb +171 -0
  27. data/lib/geometry/triangle.rb +78 -0
  28. data/lib/geometry/vector.rb +34 -0
  29. data/lib/geometry.rb +22 -0
  30. data/test/geometry/arc.rb +25 -0
  31. data/test/geometry/circle.rb +112 -0
  32. data/test/geometry/edge.rb +132 -0
  33. data/test/geometry/line.rb +132 -0
  34. data/test/geometry/obround.rb +25 -0
  35. data/test/geometry/path.rb +66 -0
  36. data/test/geometry/point.rb +258 -0
  37. data/test/geometry/point_zero.rb +177 -0
  38. data/test/geometry/polygon.rb +214 -0
  39. data/test/geometry/polyline.rb +266 -0
  40. data/test/geometry/rectangle.rb +154 -0
  41. data/test/geometry/regular_polygon.rb +120 -0
  42. data/test/geometry/rotation.rb +108 -0
  43. data/test/geometry/size.rb +97 -0
  44. data/test/geometry/size_zero.rb +153 -0
  45. data/test/geometry/square.rb +66 -0
  46. data/test/geometry/transformation/composition.rb +49 -0
  47. data/test/geometry/transformation.rb +169 -0
  48. data/test/geometry/triangle.rb +32 -0
  49. data/test/geometry/vector.rb +41 -0
  50. data/test/geometry.rb +5 -0
  51. 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