geometry 6.2 → 6.3

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.
@@ -0,0 +1,131 @@
1
+ require_relative 'point'
2
+
3
+ module Geometry
4
+ =begin rdoc
5
+ An object repesenting a {Point} that is one unit away from the origin, along each
6
+ axis, in N-dimensional space
7
+
8
+ A {PointOne} object is a {Point} that will always compare equal to one and unequal to
9
+ everything else, regardless of size. It's similar to the
10
+ {http://en.wikipedia.org/wiki/Null_Object_pattern Null Object Pattern}, but for ones.
11
+ =end
12
+ class PointOne
13
+ def eql?(other)
14
+ if other.respond_to? :all?
15
+ other.all? {|e| e.eql? 1}
16
+ else
17
+ other == 1
18
+ end
19
+ end
20
+ alias == eql?
21
+
22
+ def coerce(other)
23
+ if other.is_a? Numeric
24
+ [other, 1]
25
+ elsif other.is_a? Array
26
+ [other, Array.new(other.size, 1)]
27
+ elsif other.is_a? Vector
28
+ [other, Vector[*Array.new(other.size, 1)]]
29
+ else
30
+ [Point[other], Point[Array.new(other.size, 1)]]
31
+ end
32
+ end
33
+
34
+ def is_a?(klass)
35
+ (klass == Point) || super
36
+ end
37
+ alias :kind_of? :is_a?
38
+
39
+ # This is a hack to get Array#== to work properly. It works on ruby 2.0 and 1.9.3.
40
+ def to_ary
41
+ []
42
+ end
43
+
44
+ # @group Accessors
45
+ # @param [Integer] i Index into the {Point}'s elements
46
+ # @return [Numeric] Element i (starting at 0)
47
+ def [](i)
48
+ 1
49
+ end
50
+
51
+ # @attribute [r] x
52
+ # @return [Numeric] X-component
53
+ def x
54
+ 1
55
+ end
56
+
57
+ # @attribute [r] y
58
+ # @return [Numeric] Y-component
59
+ def y
60
+ 1
61
+ end
62
+
63
+ # @attribute [r] z
64
+ # @return [Numeric] Z-component
65
+ def z
66
+ 1
67
+ end
68
+ # @endgroup
69
+
70
+ # @group Arithmetic
71
+
72
+ # @group Unary operators
73
+ def +@
74
+ self
75
+ end
76
+
77
+ def -@
78
+ -1
79
+ end
80
+ # @endgroup
81
+
82
+ def +(other)
83
+ case other
84
+ when Numeric
85
+ Point.iso(other + 1)
86
+ when Size
87
+ Point[other.map {|a| a + 1 }]
88
+ else
89
+ if other.respond_to?(:map)
90
+ other.map {|a| a + 1 }
91
+ else
92
+ Point[other + 1]
93
+ end
94
+ end
95
+ end
96
+
97
+ def -(other)
98
+ if other.is_a? Size
99
+ Point[other.map {|a| 1 - a }]
100
+ elsif other.respond_to? :map
101
+ other.map {|a| 1 - a }
102
+ elsif other == 1
103
+ Point.zero
104
+ else
105
+ Point.iso(1 - other)
106
+ end
107
+ end
108
+
109
+ def *(other)
110
+ raise OperationNotDefined unless other.is_a? Numeric
111
+ other
112
+ end
113
+
114
+ def /(other)
115
+ raise OperationNotDefined unless other.is_a? Numeric
116
+ raise ZeroDivisionError if 0 == other
117
+ 1 / other
118
+ end
119
+ # @endgroup
120
+
121
+ # @group Enumerable
122
+
123
+ # Return the first, or first n, elements (always 0)
124
+ # @param n [Number] the number of elements to return
125
+ def first(n=nil)
126
+ Array.new(n, 1) rescue 1
127
+ end
128
+ # @endgroup
129
+ end
130
+ end
131
+
@@ -1,4 +1,5 @@
1
1
  require_relative 'point'
2
+ require_relative 'point_iso'
2
3
 
3
4
  module Geometry
4
5
  =begin rdoc
@@ -30,6 +31,11 @@ everything else, regardless of size. You can think of it as an application of th
30
31
  end
31
32
  end
32
33
 
34
+ def is_a?(klass)
35
+ (klass == Point) || super
36
+ end
37
+ alias :kind_of? :is_a?
38
+
33
39
  # This is a hack to get Array#== to work properly. It works on ruby 2.0 and 1.9.3.
34
40
  def to_ary
35
41
  []
@@ -75,7 +81,9 @@ everything else, regardless of size. You can think of it as an application of th
75
81
 
76
82
  def +(other)
77
83
  case other
78
- when Array, Numeric then other
84
+ when Array then other
85
+ when Numeric
86
+ Point.iso(other)
79
87
  else
80
88
  Point[other]
81
89
  end
@@ -84,6 +92,8 @@ everything else, regardless of size. You can think of it as an application of th
84
92
  def -(other)
85
93
  if other.is_a? Size
86
94
  -Point[other]
95
+ elsif other.is_a? Numeric
96
+ Point.iso(-other)
87
97
  elsif other.respond_to? :-@
88
98
  -other
89
99
  elsif other.respond_to? :map
@@ -102,6 +112,14 @@ everything else, regardless of size. You can think of it as an application of th
102
112
  end
103
113
  # @endgroup
104
114
 
115
+ # @group Enumerable
116
+
117
+ # Return the first, or first n, elements (always 0)
118
+ # @param n [Number] the number of elements to return
119
+ def first(n=nil)
120
+ Array.new(n, 0) rescue 0
121
+ end
122
+ # @endgroup
105
123
  end
106
124
  end
107
125
 
@@ -18,11 +18,11 @@ but there's currently nothing that enforces simplicity.
18
18
  class Polygon < Polyline
19
19
 
20
20
  # Construct a new Polygon from Points and/or Edges
21
- # The constructor will try to convert all of its arguments into Points and
22
- # Edges. Then successive Points will be collpased into Edges. Successive
23
- # Edges that share a common vertex will be added to the new Polygon. If
24
- # there's a gap between Edges it will be automatically filled with a new
25
- # Edge. The resulting Polygon will then be closed if it isn't already.
21
+ # The constructor will try to convert all of its arguments into {Points} and
22
+ # {Edges}. Then successive {Points} will be collpased into {Edges}. Successive
23
+ # {Edges} that share a common vertex will be added to the new {Polygon}. If
24
+ # there's a gap between {Edges} it will be automatically filled with a new
25
+ # {Edge}. The resulting Polygon will then be closed, if it isn't already.
26
26
  # @overload initialize(Edge, Edge, ...)
27
27
  # @return [Polygon]
28
28
  # @overload initialize(Point, Point, ...)
@@ -116,7 +116,7 @@ but there's currently nothing that enforces simplicity.
116
116
  if (a.first == b.first) and (a.last == b.last) # Equal edges
117
117
  elsif (a.first == b.last) and (a.last == b.first) # Ignore equal but opposite edges
118
118
  else
119
- if a.vector.normalize == b.vector.normalize # Same direction?
119
+ if a.direction == b.direction # Same direction?
120
120
  offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, b.first)
121
121
  offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, a.last)
122
122
  else # Opposite direction
@@ -15,6 +15,10 @@ also like a {Path} in that it isn't necessarily closed.
15
15
  class Polyline
16
16
  attr_reader :edges, :vertices
17
17
 
18
+ # @!attribute points
19
+ # @return [Array<Point>] all of the vertices of the {Polyline} (alias of #vertices)
20
+ alias :points :vertices
21
+
18
22
  # Construct a new Polyline from Points and/or Edges
19
23
  # @note The constructor will try to convert all of its arguments into {Point}s and
20
24
  # {Edge}s. Then successive {Point}s will be collpased into {Edge}s. Successive
@@ -123,6 +123,12 @@ The {Rectangle} class cluster represents your typical arrangement of 4 corners a
123
123
  Point[(max.x+min.x)/2, (max.y+min.y)/2]
124
124
  end
125
125
 
126
+ # @!attribute closed?
127
+ # @return [Bool] always true
128
+ def closed?
129
+ true
130
+ end
131
+
126
132
  # @return [Array<Edge>] The {Rectangle}'s four edges (counterclockwise)
127
133
  def edges
128
134
  point0, point2 = *@points
@@ -1,4 +1,3 @@
1
- require_relative 'cluster_factory'
2
1
  require_relative 'polygon'
3
2
 
4
3
  module Geometry
@@ -10,53 +9,49 @@ A {RegularPolygon} is a lot like a {Polygon}, but more regular.
10
9
  == Usage
11
10
  polygon = Geometry::RegularPolygon.new sides:4, center:[1,2], radius:3
12
11
  polygon = Geometry::RegularPolygon.new sides:6, center:[1,2], diameter:6
12
+
13
+ polygon = Geometry::RegularPolygon.new sides:4, center:[1,2], inradius:3
14
+ polygon = Geometry::RegularPolygon.new sides:6, center:[1,2], indiameter:6
13
15
  =end
14
16
 
15
17
  class RegularPolygon < Polygon
16
- include ClusterFactory
17
-
18
18
  # @return [Point] The {RegularPolygon}'s center point
19
19
  attr_reader :center
20
20
 
21
21
  # @return [Number] The {RegularPolygon}'s number of sides
22
22
  attr_reader :edge_count
23
23
 
24
- # @return [Number] The {RegularPolygon}'s radius
25
- attr_reader :radius
26
-
27
24
  # @overload new(sides, center, radius)
28
25
  # Construct a {RegularPolygon} using a center point and radius
29
26
  # @option options [Number] :sides The number of edges
30
27
  # @option options [Point] :center (PointZero) The center point of the {RegularPolygon}
31
- # @option options [Number] :radius The radius of the {RegularPolygon}
28
+ # @option options [Number] :radius The circumradius of the {RegularPolygon}
29
+ # @overload new(sides, center, inradius)
30
+ # Construct a {RegularPolygon} using a center point and radius
31
+ # @option options [Number] :sides The number of edges
32
+ # @option options [Point] :center (PointZero) The center point of the {RegularPolygon}
33
+ # @option options [Number] :inradius The inradius of the {RegularPolygon}
32
34
  # @overload new(sides, center, diameter)
33
35
  # Construct a {RegularPolygon} using a center point and diameter
34
36
  # @option options [Number] :sides The number of edges
35
37
  # @option options [Point] :center (PointZero) The center point of the {RegularPolygon}
36
- # @option options [Number] :diameter The diameter of the {RegularPolygon}
37
- def self.new(options={}, &block)
38
- raise ArgumentError, "RegularPolygon requires an edge count" unless options[:sides]
39
-
40
- center = options[:center]
41
- center = center ? Point[center] : Point.zero
42
-
43
- if options.has_key?(:radius)
44
- self.allocate.tap {|polygon| polygon.send :initialize, options[:sides], center, options[:radius], &block }
45
- elsif options.has_key?(:diameter)
46
- DiameterRegularPolygon.new options[:sides], center, options[:diameter], &block
47
- else
48
- raise ArgumentError, "RegularPolygon.new requires a radius or a diameter"
49
- end
50
- end
51
-
52
- # Construct a new {RegularPolygon} from a centerpoint and radius
53
- # @param [Number] edge_count The number of edges
54
- # @param [Point] center The center point of the {Circle}
55
- # @param [Number] radius The radius of the {Circle}
38
+ # @option options [Number] :diameter The circumdiameter of the {RegularPolygon}
39
+ # @overload new(sides, center, indiameter)
40
+ # Construct a {RegularPolygon} using a center point and diameter
41
+ # @option options [Number] :sides The number of edges
42
+ # @option options [Point] :center (PointZero) The center point of the {RegularPolygon}
43
+ # @option options [Number] :indiameter The circumdiameter of the {RegularPolygon}
56
44
  # @return [RegularPolygon] A new {RegularPolygon} object
57
- def initialize(edge_count, center, radius)
58
- @center = Point[center]
59
- @edge_count = edge_count
45
+ def initialize(edge_count:nil, sides:nil, center:nil, radius:nil, diameter:nil, indiameter:nil, inradius:nil)
46
+ @edge_count = edge_count || sides
47
+ raise ArgumentError, "RegularPolygon requires an edge count" unless @edge_count
48
+
49
+ raise ArgumentError, "RegularPolygon.new requires a radius or a diameter" unless diameter || indiameter || inradius || radius
50
+
51
+ @center = center ? Point[center] : Point.zero
52
+ @diameter = diameter
53
+ @indiameter = indiameter
54
+ @inradius = inradius
60
55
  @radius = radius
61
56
  end
62
57
 
@@ -65,6 +60,12 @@ A {RegularPolygon} is a lot like a {Polygon}, but more regular.
65
60
  end
66
61
  alias :== :eql?
67
62
 
63
+ # Check to see if the {Polygon} is closed (always true)
64
+ # @return [True] Always true because a {Polygon} is always closed
65
+ def closed?
66
+ true
67
+ end
68
+
68
69
  # @!group Accessors
69
70
  # @return [Rectangle] The smallest axis-aligned {Rectangle} that bounds the receiver
70
71
  def bounds
@@ -74,8 +75,9 @@ A {RegularPolygon} is a lot like a {Polygon}, but more regular.
74
75
  # @!attribute [r] diameter
75
76
  # @return [Numeric] The diameter of the {RegularPolygon}
76
77
  def diameter
77
- @radius*2
78
+ @diameter || (@radius && 2*@radius) || (@indiameter && @indiameter/cosine_half_angle)
78
79
  end
80
+ alias :circumdiameter :diameter
79
81
 
80
82
  # !@attribute [r] edges
81
83
  def edges
@@ -88,6 +90,7 @@ A {RegularPolygon} is a lot like a {Polygon}, but more regular.
88
90
  def vertices
89
91
  (0...2*Math::PI).step(2*Math::PI/edge_count).map {|angle| center + Point[Math::cos(angle), Math::sin(angle)]*radius }
90
92
  end
93
+ alias :points :vertices
91
94
 
92
95
  # @return [Point] The upper right corner of the bounding {Rectangle}
93
96
  def max
@@ -103,34 +106,38 @@ A {RegularPolygon} is a lot like a {Polygon}, but more regular.
103
106
  def minmax
104
107
  [self.min, self.max]
105
108
  end
106
- # @!endgroup
107
- end
108
-
109
- class DiameterRegularPolygon < RegularPolygon
110
- # @return [Number] The {RegularPolygon}'s diameter
111
- attr_reader :diameter
112
109
 
113
- # Construct a new {RegularPolygon} from a centerpoint and a diameter
114
- # @param [Number] edge_count The number of edges
115
- # @param [Point] center The center point of the {RegularPolygon}
116
- # @param [Number] diameter The radius of the {RegularPolygon}
117
- # @return [RegularPolygon] A new {RegularPolygon} object
118
- def initialize(edge_count, center, diameter)
119
- @center = center ? Point[center] : nil
120
- @edge_count = edge_count
121
- @diameter = diameter
110
+ # @!attribute indiameter
111
+ # @return [Number] the indiameter
112
+ def indiameter
113
+ @indiameter || (@inradius && 2*@inradius) || (@diameter && (@diameter * cosine_half_angle)) || (@radius && (2 * @radius * cosine_half_angle))
122
114
  end
123
115
 
124
- def eql?(other)
125
- (self.center == other.center) && (self.edge_count == other.edge_count) && (self.diameter == other.diameter)
116
+ # @!attribute inradius
117
+ # @return [Number] The inradius
118
+ def inradius
119
+ @inradius || (@indiameter && @indiameter/2) || (@radius && (@radius * cosine_half_angle))
126
120
  end
127
- alias :== :eql?
121
+ alias :apothem :inradius
128
122
 
129
- # @!group Accessors
130
- # @return [Number] The {RegularPolygon}'s radius
123
+ # @!attribute [r] radius
124
+ # @return [Number] The {RegularPolygon}'s radius
131
125
  def radius
132
- @diameter/2
126
+ @radius || (@diameter && @diameter/2) || (@inradius && (@inradius / cosine_half_angle)) || (@indiameter && @indiameter/cosine_half_angle/2)
127
+ end
128
+ alias :circumradius :radius
129
+
130
+ # @!attribute [r] side_length
131
+ # @return [Number] The length of each side
132
+ def side_length
133
+ 2 * circumradius * Math.sin(Math::PI/edge_count)
133
134
  end
135
+
136
+ private
137
+ def cosine_half_angle
138
+ Math.cos(Math::PI/edge_count)
139
+ end
140
+
134
141
  # @!endgroup
135
142
  end
136
143
  end
@@ -105,6 +105,7 @@ A generalized representation of a rotation transformation.
105
105
  # @param [Point] point the {Point} to rotate into the parent coordinate frame
106
106
  # @return [Point] the rotated {Point}
107
107
  def transform(point)
108
+ return point if point.is_a?(PointZero)
108
109
  m = matrix
109
110
  m ? Point[m * Point[point]] : point
110
111
  end
@@ -1,5 +1,8 @@
1
1
  require 'matrix'
2
2
 
3
+ require_relative 'size_one'
4
+ require_relative 'size_zero'
5
+
3
6
  module Geometry
4
7
  =begin
5
8
  An object representing the size of something.
@@ -29,13 +32,42 @@ methods (width, height and depth).
29
32
  array.flatten!
30
33
  super *array
31
34
  end
32
-
35
+
36
+ # Creates and returns a new {SizeOne} instance. Or, a {Size} full of ones if the size argument is given.
37
+ # @param size [Number] the size of the new {Size} full of ones
38
+ # @return [SizeOne] A new {SizeOne} instance
39
+ def self.one(size=nil)
40
+ size ? Size[Array.new(size, 1)] : SizeOne.new
41
+ end
42
+
43
+ # Creates and returns a new {SizeOne} instance. Or, a {Size} full of zeros if the size argument is given.
44
+ # @param size [Number] the size of the new {Size} full of zeros
45
+ # @return [SizeOne] A new {SizeOne} instance
46
+ def self.zero(size=nil)
47
+ size ? Size[Array.new(size, 0)] : SizeOne.new
48
+ end
49
+
33
50
  # Allow comparison with an Array, otherwise do the normal thing
34
51
  def ==(other)
35
52
  return @elements == other if other.is_a?(Array)
36
53
  super other
37
54
  end
38
55
 
56
+ # Override Vector#[] to allow for regular array slicing
57
+ def [](*args)
58
+ @elements[*args]
59
+ end
60
+
61
+ def coerce(other)
62
+ case other
63
+ when Array then [Size[*other], self]
64
+ when Numeric then [Size[Array.new(self.size, other)], self]
65
+ when Vector then [Size[*other], self]
66
+ else
67
+ raise TypeError, "#{self.class} can't be coerced into #{other.class}"
68
+ end
69
+ end
70
+
39
71
  def inspect
40
72
  'Size' + @elements.inspect
41
73
  end
@@ -91,13 +123,12 @@ methods (width, height and depth).
91
123
  left = top = -args.shift
92
124
  right = bottom = 0
93
125
  elsif 2 == args.size
94
- left = -args.shift
95
- top = -args.shift
96
- right = bottom = 0
126
+ left = right = -args.shift
127
+ top = bottom = -args.shift
97
128
  end
98
129
 
99
- left = -options[:x] if options[:x]
100
- top = -options[:y] if options[:y]
130
+ left = right = -options[:x] if options[:x]
131
+ top = bottom = -options[:y] if options[:y]
101
132
 
102
133
  top = -options[:top] if options[:top]
103
134
  left = -options[:left] if options[:left]
@@ -125,13 +156,12 @@ methods (width, height and depth).
125
156
  left = top = args.shift
126
157
  right = bottom = 0
127
158
  elsif 2 == args.size
128
- left = args.shift
129
- top = args.shift
130
- right = bottom = 0
159
+ left = right = args.shift
160
+ top = bottom = args.shift
131
161
  end
132
162
 
133
- left = options[:x] if options[:x]
134
- top = options[:y] if options[:y]
163
+ left = right = options[:x] if options[:x]
164
+ top = bottom = options[:y] if options[:y]
135
165
 
136
166
  top = options[:top] if options[:top]
137
167
  left = options[:left] if options[:left]