geometry 6.4 → 6.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ruby.yml +21 -0
  3. data/Gemfile +3 -0
  4. data/README.markdown +1 -2
  5. data/geometry.gemspec +5 -1
  6. data/lib/geometry/annulus.rb +20 -2
  7. data/lib/geometry/arc.rb +72 -1
  8. data/lib/geometry/edge.rb +21 -4
  9. data/lib/geometry/line.rb +84 -18
  10. data/lib/geometry/obround.rb +1 -2
  11. data/lib/geometry/path.rb +27 -0
  12. data/lib/geometry/point.rb +59 -8
  13. data/lib/geometry/point_iso.rb +12 -2
  14. data/lib/geometry/point_one.rb +44 -34
  15. data/lib/geometry/point_zero.rb +12 -1
  16. data/lib/geometry/polygon.rb +13 -12
  17. data/lib/geometry/polyline.rb +6 -6
  18. data/lib/geometry/rectangle.rb +12 -12
  19. data/lib/geometry/rotation.rb +5 -3
  20. data/lib/geometry/size.rb +1 -3
  21. data/lib/geometry/square.rb +13 -11
  22. data/lib/geometry/transformation/composition.rb +1 -1
  23. data/lib/geometry/transformation.rb +15 -0
  24. data/lib/geometry/triangle.rb +1 -1
  25. data/test/geometry/annulus.rb +12 -0
  26. data/test/geometry/arc.rb +98 -0
  27. data/test/geometry/circle.rb +3 -3
  28. data/test/geometry/edge.rb +16 -2
  29. data/test/geometry/line.rb +73 -0
  30. data/test/geometry/path.rb +16 -0
  31. data/test/geometry/point.rb +78 -2
  32. data/test/geometry/point_iso.rb +31 -21
  33. data/test/geometry/point_one.rb +11 -1
  34. data/test/geometry/point_zero.rb +46 -36
  35. data/test/geometry/polygon.rb +1 -1
  36. data/test/geometry/rectangle.rb +6 -1
  37. data/test/geometry/rotation.rb +1 -1
  38. data/test/geometry/size_one.rb +1 -1
  39. data/test/geometry/size_zero.rb +1 -1
  40. data/test/geometry/square.rb +15 -10
  41. data/test/geometry/transformation.rb +15 -1
  42. data/test/geometry/vector.rb +1 -1
  43. metadata +36 -9
  44. data/.travis.yml +0 -12
@@ -1,5 +1,3 @@
1
- require_relative 'point'
2
-
3
1
  module Geometry
4
2
  =begin rdoc
5
3
  An object repesenting a {Point} that is one unit away from the origin, along each
@@ -67,43 +65,55 @@ everything else, regardless of size. It's similar to the
67
65
  end
68
66
  # @endgroup
69
67
 
70
- # @override max()
71
- # @return [Number] The maximum value of the {Point}'s elements
72
- # @override max(point)
73
- # @return [Point] The element-wise maximum values of the receiver and the given {Point}
74
- def max(*args)
75
- if args.empty?
76
- 1
77
- else
78
- args = args.first if 1 == args.size
79
- Point[Array.new(args.size, 1).zip(args).map(&:max)]
68
+ # @override max()
69
+ # @return [Number] The maximum value of the {Point}'s elements
70
+ # @override max(point)
71
+ # @return [Point] The element-wise maximum values of the receiver and the given {Point}
72
+ def max(*args)
73
+ if args.empty?
74
+ 1
75
+ else
76
+ args = args.first if 1 == args.size
77
+ Point[Array.new(args.size, 1).zip(args).map(&:max)]
78
+ end
80
79
  end
81
- end
82
80
 
83
- # @override min()
84
- # @return [Number] The minimum value of the {Point}'s elements
85
- # @override min(point)
86
- # @return [Point] The element-wise minimum values of the receiver and the given {Point}
87
- def min(*args)
88
- if args.empty?
89
- 1
90
- else
91
- args = args.first if 1 == args.size
92
- Point[Array.new(args.size, 1).zip(args).map(&:min)]
81
+ # @override min()
82
+ # @return [Number] The minimum value of the {Point}'s elements
83
+ # @override min(point)
84
+ # @return [Point] The element-wise minimum values of the receiver and the given {Point}
85
+ def min(*args)
86
+ if args.empty?
87
+ 1
88
+ else
89
+ args = args.first if 1 == args.size
90
+ Point[Array.new(args.size, 1).zip(args).map(&:min)]
91
+ end
93
92
  end
94
- end
95
93
 
96
- # @override minmax()
97
- # @return [Array<Number>] The minimum value of the {Point}'s elements
98
- # @override min(point)
99
- # @return [Array<Point>] The element-wise minimum values of the receiver and the given {Point}
100
- def minmax(*args)
101
- if args.empty?
102
- [1, 1]
103
- else
104
- [min(*args), max(*args)]
94
+ # @override minmax()
95
+ # @return [Array<Number>] The minimum value of the {Point}'s elements
96
+ # @override min(point)
97
+ # @return [Array<Point>] The element-wise minimum values of the receiver and the given {Point}
98
+ def minmax(*args)
99
+ if args.empty?
100
+ [1, 1]
101
+ else
102
+ [min(*args), max(*args)]
103
+ end
104
+ end
105
+
106
+ # Returns a new {Point} with the given number of elements removed from the end
107
+ # @return [Point] the popped elements
108
+ def pop(count=1)
109
+ Point[Array.new(count, 1)]
110
+ end
111
+
112
+ # Removes the first element and returns it
113
+ # @return [Point] the shifted elements
114
+ def shift(count=1)
115
+ Point[Array.new(count, 1)]
105
116
  end
106
- end
107
117
 
108
118
  # @group Arithmetic
109
119
 
@@ -1,4 +1,3 @@
1
- require_relative 'point'
2
1
  require_relative 'point_iso'
3
2
 
4
3
  module Geometry
@@ -105,6 +104,18 @@ everything else, regardless of size. You can think of it as an application of th
105
104
  end
106
105
  end
107
106
 
107
+ # Returns a new {Point} with the given number of elements removed from the end
108
+ # @return [Point] the popped elements
109
+ def pop(count=1)
110
+ Point[Array.new(count, 0)]
111
+ end
112
+
113
+ # Removes the first element and returns it
114
+ # @return [Point] the shifted elements
115
+ def shift(count=1)
116
+ Point[Array.new(count, 0)]
117
+ end
118
+
108
119
  # @group Arithmetic
109
120
 
110
121
  # @group Unary operators
@@ -46,7 +46,7 @@ but there's currently nothing that enforces simplicity.
46
46
 
47
47
  # @return [Polygon] A new {Polygon} with orientation that's the opposite of the receiver
48
48
  def reverse
49
- self.class.new *(self.vertices.reverse)
49
+ self.class.new(*(self.vertices.reverse))
50
50
  end
51
51
 
52
52
  # Reverse the receiver and return it
@@ -68,7 +68,7 @@ but there's currently nothing that enforces simplicity.
68
68
  # @param [Point] point The {Point} to test
69
69
  # @return [Number] 1 if the {Point} is inside the {Polygon}, -1 if it's outside, and 0 if it's on an {Edge}
70
70
  def <=>(point)
71
- sum = edges.reduce(0) do |sum, e|
71
+ winding = edges.reduce(0) do |sum, e|
72
72
  direction = e.last.y <=> e.first.y
73
73
  # Ignore edges that don't cross the point's x coordinate
74
74
  next sum unless ((point.y <=> e.last.y) + (point.y <=> e.first.y)).abs <= 1
@@ -83,7 +83,7 @@ but there's currently nothing that enforces simplicity.
83
83
  sum += 0 <=> (direction + is_left)
84
84
  end
85
85
  end
86
- (0 == sum) ? -1 : 1
86
+ (0 == winding) ? -1 : 1
87
87
  end
88
88
 
89
89
  # Create a new {Polygon} that's the union of the receiver and a passed {Polygon}
@@ -152,11 +152,11 @@ but there's currently nothing that enforces simplicity.
152
152
  edgeFragments = edgeFragments.reject {|f| edgeFragments.find {|f2| (f[:first] == f2[:last]) and (f[:last] == f2[:first])} }
153
153
 
154
154
  # Construct the output polygons
155
- output = edgeFragments.reduce([Array.new]) do |output, fragment|
155
+ output_polygons = edgeFragments.reduce([Array.new]) do |output, fragment|
156
156
  next output if fragment.empty?
157
157
  polygon = output.last
158
158
  polygon.push fragment[:first], fragment[:last] if polygon.empty?
159
- while 1 do
159
+ loop do
160
160
  adjacent_fragment = edgeFragments.find {|f| fragment[:last] == f[:first]}
161
161
  break unless adjacent_fragment
162
162
 
@@ -170,13 +170,13 @@ but there's currently nothing that enforces simplicity.
170
170
  end
171
171
 
172
172
  # If everything worked properly there should be only one output Polygon
173
- output.reject! {|a| a.empty?}
174
- output = Polygon.new *(output[0])
173
+ output_polygons.reject! {|a| a.empty?}
174
+ output_polygons = Polygon.new(*(output_polygons[0]))
175
175
 
176
176
  # Table 4: Both input polygons are "island" type and the operation
177
177
  # is union, so the output polygon's orientation should be the same
178
178
  # as the input polygon's orientation
179
- (self.clockwise? != output.clockwise?) ? output.reverse : output
179
+ (self.clockwise? != output_polygons.clockwise?) ? output_polygons.reverse : output_polygons
180
180
  end
181
181
  alias :+ :union
182
182
 
@@ -213,7 +213,7 @@ but there's currently nothing that enforces simplicity.
213
213
  break if current_point == hull_points.first
214
214
  hull_points << min_point
215
215
  end
216
- Polygon.new *hull_points
216
+ Polygon.new(*hull_points)
217
217
  end
218
218
 
219
219
  # @endgroup
@@ -278,7 +278,7 @@ but there's currently nothing that enforces simplicity.
278
278
  redo # Recheck the modified edges
279
279
  end
280
280
  end
281
- Polygon.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten)
281
+ Polygon.new(*(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten))
282
282
  end
283
283
 
284
284
  # Vertex bisectors suitable for outsetting
@@ -332,9 +332,10 @@ but there's currently nothing that enforces simplicity.
332
332
  # @param [Integer] index The index to insert the new {Point} before
333
333
  # @param [Point] point The {Point} to insert
334
334
  # @param [Integer] type The vertex type: 1 is inside, 0 is boundary, -1 is outside
335
+ # @return [Bool] true if the {Point} was inserted, false otherwise
335
336
  def insert(index, point, type)
336
- if v = @vertices.find {|v| v[:vertex] == point }
337
- v[:type] = type
337
+ if vertex = @vertices.find {|v| v[:vertex] == point }
338
+ vertex[:type] = type
338
339
  false
339
340
  else
340
341
  @vertices.insert(index, {:vertex => point, :type => type})
@@ -41,7 +41,7 @@ also like a {Path} in that it isn't necessarily closed.
41
41
  @vertices.push first
42
42
  elsif first.is_a?(Edge)
43
43
  @edges.push first
44
- @vertices.push *(first.to_a)
44
+ @vertices.push(*(first.to_a))
45
45
  end
46
46
 
47
47
  args.reduce(@vertices.last) do |previous,n|
@@ -82,7 +82,7 @@ also like a {Path} in that it isn't necessarily closed.
82
82
  else
83
83
  e = Edge.new(previous, n.first)
84
84
  push_edge e, n
85
- push_vertex *(e.to_a), *(n.to_a)
85
+ push_vertex(*(e.to_a), *(n.to_a))
86
86
  end
87
87
  n.last
88
88
  end
@@ -167,7 +167,7 @@ also like a {Path} in that it isn't necessarily closed.
167
167
  # Clone the receiver, reverse it, then return it
168
168
  # @return [Polyline] the reversed clone
169
169
  def reverse
170
- self.class.new *(edges.reverse.map! {|edge| edge.reverse! })
170
+ self.class.new(*(edges.reverse.map! {|edge| edge.reverse! }))
171
171
  end
172
172
 
173
173
  # Reverse the receiver and return it
@@ -261,7 +261,7 @@ also like a {Path} in that it isn't necessarily closed.
261
261
  redo # Recheck the modified edges
262
262
  end
263
263
  end
264
- Polyline.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten)
264
+ Polyline.new(*(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten))
265
265
  end
266
266
  alias :leftset :offset
267
267
 
@@ -369,12 +369,12 @@ also like a {Path} in that it isn't necessarily closed.
369
369
  end
370
370
 
371
371
  def push_edge(*e)
372
- @edges.push *e
372
+ @edges.push(*e)
373
373
  @edges.uniq!
374
374
  end
375
375
 
376
376
  def push_vertex(*v)
377
- @vertices.push *v
377
+ @vertices.push(*v)
378
378
  @vertices.uniq!
379
379
  end
380
380
 
@@ -25,16 +25,8 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
25
25
  class Rectangle
26
26
  include ClusterFactory
27
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
28
  # @return [Size] The {Size} of the {Rectangle}
35
29
  attr_reader :size
36
- # @return [Number] Width of the {Rectangle}
37
- attr_reader :width
38
30
 
39
31
  # @overload new(width, height)
40
32
  # Creates a {Rectangle} of the given width and height, centered on the origin
@@ -120,7 +112,7 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
120
112
  # @return [Point] The {Rectangle}'s center
121
113
  def center
122
114
  min, max = @points.minmax {|a,b| a.y <=> b.y}
123
- Point[(max.x+min.x)/2, (max.y+min.y)/2]
115
+ Point[(max.x+min.x).to_r/2, (max.y+min.y).to_r/2]
124
116
  end
125
117
 
126
118
  # @!attribute closed?
@@ -155,25 +147,28 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
155
147
  [self.min, self.max]
156
148
  end
157
149
 
158
- # @return [Array<Point>] The {Rectangle}'s four points (counterclockwise)
150
+ # @return [Array<Point>] The {Rectangle}'s four points (clockwise)
159
151
  def points
160
152
  point0, point2 = *@points
161
- point1 = Point[point2.x, point0.y]
162
- point3 = Point[point0.x, point2.y]
153
+ point1 = Point[point0.x, point2.y]
154
+ point3 = Point[point2.x, point0.y]
163
155
  [point0, point1, point2, point3]
164
156
  end
165
157
 
158
+ # @return [Point] The {Rectangle}'s origin
166
159
  def origin
167
160
  minx = @points.min {|a,b| a.x <=> b.x}
168
161
  miny = @points.min {|a,b| a.y <=> b.y}
169
162
  Point[minx.x, miny.y]
170
163
  end
171
164
 
165
+ # @return [Number] Height of the {Rectangle}
172
166
  def height
173
167
  min, max = @points.minmax {|a,b| a.y <=> b.y}
174
168
  max.y - min.y
175
169
  end
176
170
 
171
+ # @return [Number] Width of the {Rectangle}
177
172
  def width
178
173
  min, max = @points.minmax {|a,b| a.x <=> b.x}
179
174
  max.x - min.x
@@ -212,6 +207,11 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
212
207
  Rectangle.new from:(min + Point[options[:left], options[:bottom]]), to:(max - Point[options[:right], options[:top]])
213
208
  end
214
209
  end
210
+
211
+ # @return [Path] A closed {Path} that traces the boundary of the {Rectangle} clockwise, starting from the lower-left
212
+ def path
213
+ Path.new(*self.points, self.points.first)
214
+ end
215
215
  end
216
216
 
217
217
  class CenteredRectangle < Rectangle
@@ -72,16 +72,18 @@ A generalized representation of a rotation transformation.
72
72
  alias :== :eql?
73
73
 
74
74
  def identity?
75
- (!@x && !@y && !@z) || ([@x, @y, @z].select {|a| a}.all? {|a| a.respond_to?(:magnitude) ? (1 == a.magnitude) : (1 == a.size)})
75
+ x, y, z = self.x, self.y, self.z
76
+ (!x && !y && !z) || ([x, y, z].select {|a| a}.all? {|a| a.respond_to?(:magnitude) ? (1 == a.magnitude) : (1 == a.size)})
76
77
  end
77
78
 
78
79
  # @attribute [r] matrix
79
80
  # @return [Matrix] the transformation {Matrix} representing the {Rotation}
80
81
  def matrix
81
- return nil unless [@x, @y, @z].compact.size >= 2
82
+ x, y, z = self.x, self.y, self.z
83
+ return nil unless [x, y, z].compact.size >= 2
82
84
 
83
85
  # Force all axes to be Vectors
84
- x,y,z = [@x, @y, @z].map {|a| a.is_a?(Array) ? Vector[*a] : a}
86
+ x,y,z = [x, y, z].map {|a| a.is_a?(Array) ? Vector[*a] : a}
85
87
 
86
88
  # Force all axes to exist
87
89
  if x and y
data/lib/geometry/size.rb CHANGED
@@ -17,8 +17,6 @@ methods (width, height and depth).
17
17
  =end
18
18
 
19
19
  class Size < Vector
20
- attr_reader :x, :y, :z
21
-
22
20
  # Allow vector-style initialization, but override to support copy-init
23
21
  # from Vector, Point or another Size
24
22
  #
@@ -30,7 +28,7 @@ methods (width, height and depth).
30
28
  def self.[](*array)
31
29
  array.map! {|a| a.respond_to?(:to_a) ? a.to_a : a }
32
30
  array.flatten!
33
- super *array
31
+ super(*array)
34
32
  end
35
33
 
36
34
  # Creates and returns a new {SizeOne} instance. Or, a {Size} full of ones if the size argument is given.
@@ -16,10 +16,6 @@ The {Square} class cluster is like the {Rectangle} class cluster, but not longer
16
16
  class Square
17
17
  include ClusterFactory
18
18
 
19
- # @!attribute origin
20
- # @return [Point] The {Square}'s origin
21
- attr_reader :origin
22
-
23
19
  # @!attribute points
24
20
  # @return [Array<Point>] the corner {Point}s of the {Square} in counter-clockwise order
25
21
  attr_reader :points
@@ -101,15 +97,16 @@ The {Square} class cluster is like the {Rectangle} class cluster, but not longer
101
97
  [self.min, self.max]
102
98
  end
103
99
 
104
- # @attribute [r] origin
105
- # @return [Point] The lower left corner
100
+ # @!attribute origin
101
+ # @return [Point] The {Square}'s origin (lower-left corner)
106
102
  def origin
107
103
  @points.first
108
104
  end
109
105
 
106
+ # @return [Array<Point>] The {Square}'s four points (clockwise)
110
107
  def points
111
108
  p0, p1 = *@points
112
- [p0, Point[p1.x, p0.y], p1, Point[p0.x, p1.y]]
109
+ [p0, Point[p0.x, p1.y], p1, Point[p1.x, p0.y]]
113
110
  end
114
111
 
115
112
  def height
@@ -122,6 +119,11 @@ The {Square} class cluster is like the {Rectangle} class cluster, but not longer
122
119
  max.x - min.x
123
120
  end
124
121
  # @endgroup
122
+
123
+ # @return [Path] A closed {Path} that traces the boundary of the {Square} clockwise, starting from the lower-left
124
+ def path
125
+ Path.new(*self.points, self.points.first)
126
+ end
125
127
  end
126
128
 
127
129
  # A {Square} created with a center point and a size
@@ -166,7 +168,7 @@ The {Square} class cluster is like the {Rectangle} class cluster, but not longer
166
168
  end
167
169
 
168
170
  # @attribute [r] points
169
- # @return [Array<Point>] The {Square}'s four points (counterclockwise)
171
+ # @return [Array<Point>] The {Square}'s four points (clockwise)
170
172
  def points
171
173
  half_size = @size/2
172
174
  minx = @center.x - half_size
@@ -174,7 +176,7 @@ The {Square} class cluster is like the {Rectangle} class cluster, but not longer
174
176
  miny = @center.y - half_size
175
177
  maxy = @center.y + half_size
176
178
 
177
- [Point[minx,miny], Point[maxx, miny], Point[maxx, maxy], Point[minx,maxy]]
179
+ [Point[minx,miny], Point[minx, maxy], Point[maxx, maxy], Point[maxx,miny]]
178
180
  end
179
181
 
180
182
  def height
@@ -224,14 +226,14 @@ The {Square} class cluster is like the {Rectangle} class cluster, but not longer
224
226
  end
225
227
 
226
228
  # @attribute [r] points
227
- # @return [Array<Point>] The {Square}'s four points (counterclockwise)
229
+ # @return [Array<Point>] The {Square}'s four points (clockwise)
228
230
  def points
229
231
  minx = origin.x
230
232
  maxx = origin.x + size
231
233
  miny = origin.y
232
234
  maxy = origin.y + size
233
235
 
234
- [origin, Point[maxx, miny], Point[maxx, maxy], Point[minx,maxy]]
236
+ [origin, Point[minx, maxy], Point[maxx, maxy], Point[maxx,miny]]
235
237
  end
236
238
 
237
239
  # @return [Number] The size of the {Square} along the y-axis
@@ -32,7 +32,7 @@ module Geometry
32
32
  # @endgroup
33
33
 
34
34
  def transform(point)
35
- transformations.reverse.reduce(point) {|point, transformation| transformation.transform(point) }
35
+ transformations.reverse.reduce(point) {|_point, transformation| transformation.transform(_point) }
36
36
  end
37
37
  end
38
38
  end
@@ -180,4 +180,19 @@ system's X-axis:
180
180
  @translation ? (@translation + point) : point
181
181
  end
182
182
  end
183
+
184
+ # @override translation(x, y, z)
185
+ # Construct a new translation from the given values
186
+ # @param x [Number] The distance to translate along the X-axis
187
+ # @param y [Number] The distance to translate along the Y-axis
188
+ # @param z [Number] The distance to translate along the Z-axis
189
+ # @override translation([x, y, z])
190
+ # Construct a new translation from an array of values
191
+ # @override translation(point)
192
+ # Construct a new translation from a {Point}
193
+ # @param point [Point] A {Point}
194
+ def self.translation(*args)
195
+ args = *args if args[0].is_a? Array
196
+ Transformation.new translate:args
197
+ end
183
198
  end
@@ -30,7 +30,7 @@ An isoscoles right {Triangle} created with an origin and leg length
30
30
  # @param [Number] height The length of the {Triangle}'s vertical leg
31
31
  def self.new(*args)
32
32
  if args.size == 3
33
- ScaleneTriangle.new *args
33
+ ScaleneTriangle.new(*args)
34
34
  elsif args.size == 2
35
35
  RightTriangle.new args[0], args[1], args[1]
36
36
  end
@@ -16,6 +16,18 @@ describe Geometry::Annulus do
16
16
  it 'must have a center' do
17
17
  subject.center.must_equal Point[1,2]
18
18
  end
19
+
20
+ it 'must have a max' do
21
+ subject.max.must_equal Point[11, 12]
22
+ end
23
+
24
+ it 'must have a min' do
25
+ subject.min.must_equal Point[-9, -8]
26
+ end
27
+
28
+ it 'must have a min and a max' do
29
+ subject.minmax.must_equal [subject.min, subject.max]
30
+ end
19
31
  end
20
32
 
21
33
  describe 'when constructed with a center, inner_radius and radius' do
data/test/geometry/arc.rb CHANGED
@@ -23,3 +23,101 @@ describe Geometry::Arc do
23
23
  end
24
24
  end
25
25
  end
26
+
27
+ describe Geometry::ThreePointArc do
28
+ it 'must have an ending angle' do
29
+ arc = Geometry::ThreePointArc.new([0,0], [1,0], [0,1])
30
+ arc.end_angle.must_equal Math::PI/2
31
+ end
32
+
33
+ it 'must have a radius' do
34
+ arc = Geometry::ThreePointArc.new([0,0], [1,0], [0,1])
35
+ arc.radius.must_equal 1
36
+ end
37
+
38
+ it 'must have an starting angle' do
39
+ arc = Geometry::ThreePointArc.new([0,0], [1,0], [0,1])
40
+ arc.start_angle.must_equal 0
41
+ end
42
+
43
+ describe 'max' do
44
+ # Cosine and sine of a 22.5 degree angle
45
+ let(:cos) { 0.9239556995 }
46
+ let(:sin) { 0.3824994973 }
47
+ let(:radius) { 1.0000000000366434 }
48
+
49
+ it 'must handle an Arc entirely in quadrant 1' do
50
+ arc = Geometry::ThreePointArc.new([0,0], [cos,sin], [sin,cos])
51
+ arc.max.must_equal Point[cos,cos]
52
+ end
53
+
54
+ it 'must handle a counterclockwise Arc from quadrant 1 to quadrant 2' do
55
+ arc = Geometry::ThreePointArc.new([0,0], [cos,sin], [-cos,sin])
56
+ arc.max.must_equal Point[cos,radius]
57
+ end
58
+
59
+ it 'must handle a counterclockwise Arc from quadrant 4 to quadrant 1' do
60
+ arc = Geometry::ThreePointArc.new([0,0], [sin,-cos], [sin,cos])
61
+ arc.max.must_equal Point[radius,cos]
62
+ end
63
+
64
+ it 'must handle a counterclockwise Arc from quadrant 3 to quadrant 2' do
65
+ arc = Geometry::ThreePointArc.new([0,0], [-cos,-sin], [-cos,sin])
66
+ arc.max.must_equal Point[radius,radius]
67
+ end
68
+ end
69
+
70
+ describe 'min' do
71
+ # Cosine and sine of a 22.5 degree angle
72
+ let(:cos) { 0.9239556995 }
73
+ let(:sin) { 0.3824994973 }
74
+ let(:radius) { 1.0000000000366434 }
75
+
76
+ it 'must handle an Arc entirely in quadrant 1' do
77
+ arc = Geometry::ThreePointArc.new([0,0], [cos,sin], [sin,cos])
78
+ arc.min.must_equal Point[sin,sin]
79
+ end
80
+
81
+ it 'must handle a counterclockwise Arc from quadrant 1 to quadrant 2' do
82
+ arc = Geometry::ThreePointArc.new([0,0], [cos,sin], [-cos,sin])
83
+ arc.min.must_equal Point[-cos,sin]
84
+ end
85
+
86
+ it 'must handle a counterclockwise Arc from quadrant 4 to quadrant 1' do
87
+ arc = Geometry::ThreePointArc.new([0,0], [sin,-cos], [sin,cos])
88
+ arc.min.must_equal Point[sin,-cos]
89
+ end
90
+
91
+ it 'must handle a counterclockwise Arc from quadrant 3 to quadrant 2' do
92
+ arc = Geometry::ThreePointArc.new([0,0], [-cos,-sin], [-cos,sin])
93
+ arc.min.must_equal Point[-cos,-radius]
94
+ end
95
+ end
96
+
97
+ describe 'minmax' do
98
+ # Cosine and sine of a 22.5 degree angle
99
+ let(:cos) { 0.9239556995 }
100
+ let(:sin) { 0.3824994973 }
101
+ let(:radius) { 1.0000000000366434 }
102
+
103
+ it 'must handle an Arc entirely in quadrant 1' do
104
+ arc = Geometry::ThreePointArc.new([0,0], [cos,sin], [sin,cos])
105
+ arc.minmax.must_equal [Point[sin,sin], Point[cos,cos]]
106
+ end
107
+
108
+ it 'must handle a counterclockwise Arc from quadrant 1 to quadrant 2' do
109
+ arc = Geometry::ThreePointArc.new([0,0], [cos,sin], [-cos,sin])
110
+ arc.minmax.must_equal [Point[-cos,sin], Point[cos,radius]]
111
+ end
112
+
113
+ it 'must handle a counterclockwise Arc from quadrant 4 to quadrant 1' do
114
+ arc = Geometry::ThreePointArc.new([0,0], [sin,-cos], [sin,cos])
115
+ arc.minmax.must_equal [Point[sin,-cos], Point[radius,cos]]
116
+ end
117
+
118
+ it 'must handle a counterclockwise Arc from quadrant 3 to quadrant 2' do
119
+ arc = Geometry::ThreePointArc.new([0,0], [-cos,-sin], [-cos,sin])
120
+ arc.minmax.must_equal [Point[-cos,-radius], Point[radius,radius]]
121
+ end
122
+ end
123
+ end
@@ -108,17 +108,17 @@ describe Geometry::Circle do
108
108
  let(:circle) { Circle.new :diameter => Rational(5,3) }
109
109
 
110
110
  it 'must have the correct min values' do
111
- circle.min.must_equal Point[-5/6, -5/6]
111
+ circle.min.must_equal Point[-5.to_r/6, -5.to_r/6]
112
112
  circle.min.must_be_instance_of Geometry::PointIso
113
113
  end
114
114
 
115
115
  it 'must have the correct max values' do
116
- circle.max.must_equal Point[5/6, 5/6]
116
+ circle.max.must_equal Point[5.to_r/6, 5.to_r/6]
117
117
  circle.max.must_be_instance_of Geometry::PointIso
118
118
  end
119
119
 
120
120
  it 'must have the correct minmax values' do
121
- circle.minmax.must_equal [Point[-5/6, -5/6], Point[5/6,5/6]]
121
+ circle.minmax.must_equal [Point[-5.to_r/6, -5.to_r/6], Point[5.to_r/6,5.to_r/6]]
122
122
  end
123
123
  end
124
124
 
@@ -75,11 +75,25 @@ describe Geometry::Edge do
75
75
  subject.to_a.must_equal original.reverse
76
76
  end
77
77
 
78
+ describe 'attributes' do
79
+ it 'must have a maximum' do
80
+ Edge([0,0], [1,1]).max.must_equal Point[1,1]
81
+ end
82
+
83
+ it 'must have a minimum' do
84
+ Edge([0,0], [1,1]).min.must_equal Point[0,0]
85
+ end
86
+
87
+ it 'must have a minmax' do
88
+ Edge([0,0], [1,1]).minmax.must_equal [Point[0,0], Point[1,1]]
89
+ end
90
+ end
91
+
78
92
  describe "spaceship" do
79
93
  it "ascending with a Point" do
80
94
  edge = Edge.new [0,0], [1,1]
81
95
  (edge <=> Point[0,0]).must_equal 0
82
- (edge <=> Point[1,0]).must_equal -1
96
+ (edge <=> Point[1,0]).must_equal(-1)
83
97
  (edge <=> Point[0,1]).must_equal 1
84
98
  (edge <=> Point[2,2]).must_equal nil
85
99
  end
@@ -88,7 +102,7 @@ describe Geometry::Edge do
88
102
  edge = Edge.new [1,1], [0,0]
89
103
  (edge <=> Point[0,0]).must_equal 0
90
104
  (edge <=> Point[1,0]).must_equal 1
91
- (edge <=> Point[0,1]).must_equal -1
105
+ (edge <=> Point[0,1]).must_equal(-1)
92
106
  (edge <=> Point[2,2]).must_equal nil
93
107
  end
94
108
  end