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