aurora-geometry 0.0.2

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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/Gemfile +7 -0
  4. data/LICENSE +21 -0
  5. data/README.markdown +105 -0
  6. data/Rakefile +24 -0
  7. data/aurora-geometry.gemspec +23 -0
  8. data/lib/geometry.rb +22 -0
  9. data/lib/geometry/arc.rb +94 -0
  10. data/lib/geometry/circle.rb +122 -0
  11. data/lib/geometry/cluster_factory.rb +15 -0
  12. data/lib/geometry/edge.rb +140 -0
  13. data/lib/geometry/line.rb +154 -0
  14. data/lib/geometry/obround.rb +238 -0
  15. data/lib/geometry/path.rb +67 -0
  16. data/lib/geometry/point.rb +163 -0
  17. data/lib/geometry/point_zero.rb +107 -0
  18. data/lib/geometry/polygon.rb +368 -0
  19. data/lib/geometry/polyline.rb +318 -0
  20. data/lib/geometry/rectangle.rb +378 -0
  21. data/lib/geometry/regular_polygon.rb +136 -0
  22. data/lib/geometry/rotation.rb +190 -0
  23. data/lib/geometry/size.rb +75 -0
  24. data/lib/geometry/size_zero.rb +70 -0
  25. data/lib/geometry/square.rb +113 -0
  26. data/lib/geometry/text.rb +24 -0
  27. data/lib/geometry/transformation.rb +171 -0
  28. data/lib/geometry/transformation/composition.rb +39 -0
  29. data/lib/geometry/triangle.rb +78 -0
  30. data/lib/geometry/vector.rb +34 -0
  31. data/test/geometry.rb +5 -0
  32. data/test/geometry/arc.rb +25 -0
  33. data/test/geometry/circle.rb +112 -0
  34. data/test/geometry/edge.rb +132 -0
  35. data/test/geometry/line.rb +132 -0
  36. data/test/geometry/obround.rb +25 -0
  37. data/test/geometry/path.rb +66 -0
  38. data/test/geometry/point.rb +258 -0
  39. data/test/geometry/point_zero.rb +177 -0
  40. data/test/geometry/polygon.rb +214 -0
  41. data/test/geometry/polyline.rb +266 -0
  42. data/test/geometry/rectangle.rb +154 -0
  43. data/test/geometry/regular_polygon.rb +120 -0
  44. data/test/geometry/rotation.rb +108 -0
  45. data/test/geometry/size.rb +97 -0
  46. data/test/geometry/size_zero.rb +153 -0
  47. data/test/geometry/square.rb +66 -0
  48. data/test/geometry/transformation.rb +169 -0
  49. data/test/geometry/transformation/composition.rb +49 -0
  50. data/test/geometry/triangle.rb +32 -0
  51. data/test/geometry/vector.rb +41 -0
  52. metadata +115 -0
@@ -0,0 +1,15 @@
1
+ # Include this module in the base class of a class cluster to handle swizzling
2
+ # of ::new
3
+ module ClusterFactory
4
+ def self.included(parent)
5
+ class << parent
6
+ alias :original_new :new
7
+
8
+ def inherited(subclass)
9
+ class << subclass
10
+ alias :new :original_new
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,140 @@
1
+ require 'mathn'
2
+
3
+ require_relative 'point'
4
+
5
+ module Geometry
6
+
7
+ =begin rdoc
8
+ An edge. It's a line segment between 2 points. Generally part of a {Polygon}.
9
+
10
+ == Usage
11
+ edge = Geometry::Edge([1,1], [2,2])
12
+
13
+ =end
14
+
15
+ class Edge
16
+ attr_reader :first, :last
17
+
18
+ # Construct a new {Edge} object from any two things that can be converted
19
+ # to a {Point}.
20
+ def initialize(point0, point1)
21
+ @first, @last = [Point[point0], Point[point1]]
22
+ end
23
+
24
+ # Two Edges are equal if both have equal {Point}s in the same order
25
+ def ==(other)
26
+ (@first == other.first) && (@last == other.last)
27
+ end
28
+
29
+ # @param [Point] point A {Point} to spaceship with
30
+ # @return [Boolean] Returns 1 if the {Point} is strictly to the left of the receiver, -1 to the right, and 0 if the point is on the receiver
31
+ def <=>(point)
32
+ case point
33
+ when Point
34
+ k = (@last.x - @first.x) * (point.y - @first.y) - (point.x - @first.x) * (@last.y - @first.y)
35
+ if 0 == k
36
+ (((@first.x <=> point.x) + (@last.x <=> point.x)).abs <= 1) && (((@first.y <=> point.y) + (@last.y <=> point.y)).abs <= 1) ? 0 : nil
37
+ else
38
+ k <=> 0
39
+ end
40
+ else
41
+ raise ArgumentError, "Can't spaceship with #{point.class}"
42
+ end
43
+ end
44
+
45
+ # Return a new {Edge} with swapped endpoints
46
+ def reverse
47
+ Edge.new(@last, @first)
48
+ end
49
+
50
+ # In-place swap the endpoints
51
+ def reverse!
52
+ @first, @last = @last, @first
53
+ self
54
+ end
55
+
56
+ # Return the {Edge}'s length along the Y axis
57
+ def height
58
+ (@first.y - @last.y).abs
59
+ end
60
+
61
+ # Return the {Edge}'s length along the X axis
62
+ def width
63
+ (@first.x - @last.x).abs
64
+ end
65
+
66
+ def inspect
67
+ 'Edge(' + @first.inspect + ', ' + @last.inspect + ')'
68
+ end
69
+ alias :to_s :inspect
70
+
71
+ # @return [Bool] Returns true if the passed {Edge} is parallel to the receiver
72
+ def parallel?(edge)
73
+ v1, v2 = self.direction, edge.direction
74
+ winding = v1[0]*v2[1] - v1[1]*v2[0]
75
+ if 0 == winding # collinear?
76
+ if v1 == v2
77
+ 1 # same direction
78
+ else
79
+ -1 # opposite direction
80
+ end
81
+ else
82
+ false
83
+ end
84
+ end
85
+
86
+ # @param [Edge] other The other {Edge} to check
87
+ # @return [Bool] Returns true if the receiver and the passed {Edge} share an endpoint
88
+ def connected?(other)
89
+ (@first == other.last) || (@last == other.first) || (@first == other.first) || (@last == other.last)
90
+ end
91
+
92
+ # @return [Vector] A unit {Vector} pointing from first to last
93
+ def direction
94
+ self.vector.normalize
95
+ end
96
+
97
+ # Find the intersection of two {Edge}s (http://bloggingmath.wordpress.com/2009/05/29/line-segment-intersection/)
98
+ # @param [Edge] other The other {Edge}
99
+ # @return [Point] The intersection of the two {Edge}s, nil if they don't intersect, true if they're collinear and overlapping, and false if they're collinear and non-overlapping
100
+ def intersection(other)
101
+ return self.first if (self.first == other.first) or (self.first == other.last)
102
+ return self.last if (self.last == other.first) or (self.last == other.last)
103
+
104
+ p0, p1 = self.first, self.last
105
+ p2, p3 = other.first, other.last
106
+ v1, v2 = self.vector, other.vector
107
+
108
+ denominator = v1[0] * v2[1] - v2[0] * v1[1] # v1 x v2
109
+ p = p0 - p2
110
+ if denominator == 0 # collinear, so check for overlap
111
+ if 0 == (-v1[1] * p.x + v1[0] * p.y) # collinear?
112
+ # The edges are collinear, but do they overlap?
113
+ # Project them onto the x and y axes to find out
114
+ left1, right1 = [self.first[0], self.last[0]].sort
115
+ bottom1, top1 = [self.first[1], self.last[1]].sort
116
+ left2, right2 = [other.first[0], other.last[0]].sort
117
+ bottom2, top2 = [other.first[1], other.last[1]].sort
118
+
119
+ !((left2 > right1) || (right2 < left1) || (top2 < bottom1) || (bottom2 > top1))
120
+ else
121
+ nil
122
+ end
123
+ else
124
+ s = (-v1[1] * p.x + v1[0] * p.y) / denominator # v1 x (p0 - p2) / denominator
125
+ t = ( v2[0] * p.y - v2[1] * p.x) / denominator # v2 x (p0 - p2) / denominator
126
+
127
+ p0 + v1 * t if ((0..1) === s) && ((0..1) === t)
128
+ end
129
+ end
130
+
131
+ # @return [Vector] A {Vector} pointing from first to last
132
+ def vector
133
+ Vector[*((last-first).to_a)]
134
+ end
135
+
136
+ def to_a
137
+ [@first, @last]
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,154 @@
1
+ require_relative 'cluster_factory'
2
+ require_relative 'point'
3
+
4
+ module Geometry
5
+
6
+ =begin rdoc
7
+ A cluster of objects representing a Line of infinite length
8
+
9
+ Supports two-point, slope-intercept, and point-slope initializer forms
10
+
11
+ == Usage
12
+
13
+ === Two-point constructors
14
+ line = Geometry::Line[[0,0], [10,10]]
15
+ line = Geometry::Line[Geometry::Point[0,0], Geometry::Point[10,10]]
16
+ line = Geometry::Line[Vector[0,0], Vector[10,10]]
17
+
18
+ === Slope-intercept constructors
19
+ Geometry::Line[Rational(3,4), 5] # Slope = 3/4, Intercept = 5
20
+ Geometry::Line[0.75, 5]
21
+
22
+ === Point-slope constructors
23
+ Geometry::Line(Geometry::Point[0,0], 0.75)
24
+ Geometry::Line(Vector[0,0], Rational(3,4))
25
+
26
+ === Special constructors (2D only)
27
+ Geometry::Line.horizontal(y=0)
28
+ Geometry::Line.vertical(x=0)
29
+ =end
30
+
31
+ class Line
32
+ include ClusterFactory
33
+
34
+ # @overload [](Array, Array)
35
+ # @return [TwoPointLine]
36
+ # @overload [](Point, Point)
37
+ # @return [TwoPointLine]
38
+ # @overload [](Vector, Vector)
39
+ # @return [TwoPointLine]
40
+ # @overload [](y-intercept, slope)
41
+ # @return [SlopeInterceptLine]
42
+ # @overload [](point, slope)
43
+ # @return [PointSlopeLine]
44
+ def self.[](*args)
45
+ if( 2 == args.size )
46
+ args.map! {|x| x.is_a?(Array) ? Point[*x] : x}
47
+
48
+ # If both args are Points, create a TwoPointLine
49
+ return TwoPointLine.new(*args) if args.all? {|x| x.is_a?(Vector)}
50
+
51
+ # If only the first arg is a Point, create a PointSlopeLine
52
+ return PointSlopeLine.new(*args) if args.first.is_a?(Vector)
53
+
54
+ # Otherise, create a SlopeInterceptLine
55
+ return SlopeInterceptLine.new(*args)
56
+ else
57
+ nil
58
+ end
59
+ end
60
+
61
+ # @overload new(from, to)
62
+ # @option options [Point] :from A starting {Point}
63
+ # @option options [Point] :to An end {Point}
64
+ # @return [TwoPointLine]
65
+ # @overload new(start, end)
66
+ # @option options [Point] :start A starting {Point}
67
+ # @option options [Point] :end An end {Point}
68
+ # @return [TwoPointLine]
69
+ def self.new(options={})
70
+ from = options[:from] || options[:start]
71
+ to = options[:end] || options[:to]
72
+
73
+ if from and to
74
+ TwoPointLine.new(from, to)
75
+ else
76
+ raise ArgumentError, "Start and end Points must be provided"
77
+ end
78
+ end
79
+
80
+ def self.horizontal(y_intercept=0)
81
+ SlopeInterceptLine.new(0, y_intercept)
82
+ end
83
+ def self.vertical(x_intercept=0)
84
+ SlopeInterceptLine.new(1/0.0, x_intercept)
85
+ end
86
+ end
87
+
88
+ # @private
89
+ class PointSlopeLine < Line
90
+ # @return [Number] the slope of the {Line}
91
+ attr_reader :slope
92
+
93
+ def initialize(point, slope)
94
+ @point = Point[point]
95
+ @slope = slope
96
+ end
97
+ def to_s
98
+ 'Line(' + @slope.to_s + ',' + @point.to_s + ')'
99
+ end
100
+ end
101
+
102
+ # @private
103
+ class SlopeInterceptLine < Line
104
+ # @return [Number] the slope of the {Line}
105
+ attr_reader :slope
106
+
107
+ def initialize(slope, intercept)
108
+ @slope = slope
109
+ @intercept = intercept
110
+ end
111
+
112
+ def horizontal?
113
+ 0 == @slope
114
+ end
115
+ def vertical?
116
+ (1/0.0) == @slope
117
+ end
118
+
119
+ def intercept(axis=:y)
120
+ case axis
121
+ when :x
122
+ vertical? ? @intercept : (horizontal? ? nil : (-@intercept/@slope))
123
+ when :y
124
+ vertical? ? nil : @intercept
125
+ end
126
+ end
127
+
128
+ def to_s
129
+ 'Line(' + @slope.to_s + ',' + @intercept.to_s + ')'
130
+ end
131
+ end
132
+
133
+ # @private
134
+ class TwoPointLine < Line
135
+ attr_reader :first, :last
136
+
137
+ def initialize(point0, point1)
138
+ @first, @last = [Point[point0], Point[point1]]
139
+ end
140
+ def inspect
141
+ 'Line(' + @first.inspect + ', ' + @last.inspect + ')'
142
+ end
143
+ alias :to_s :inspect
144
+
145
+ # @group Accessors
146
+ # !@attribute [r[ slope
147
+ # @return [Number] the slope of the {Line}
148
+ def slope
149
+ (last.y - first.y)/(last.x - first.x)
150
+ end
151
+ # @endgroup
152
+ end
153
+ end
154
+
@@ -0,0 +1,238 @@
1
+ require_relative 'cluster_factory'
2
+ require_relative 'point'
3
+
4
+ module Geometry
5
+
6
+ =begin
7
+ The {Obround} class cluster represents a rectangle with semicircular end caps
8
+
9
+ {http://en.wiktionary.org/wiki/obround}
10
+ =end
11
+
12
+ class Obround
13
+ include ClusterFactory
14
+
15
+ # @overload new(width, height)
16
+ # Creates a {Obround} of the given width and height, centered on the origin
17
+ # @param [Number] height Height
18
+ # @param [Number] width Width
19
+ # @return [CenteredObround]
20
+ # @overload new(size)
21
+ # Creates a {Obround} of the given {Size} centered on the origin
22
+ # @param [Size] size Width and height
23
+ # @return [CenteredObround]
24
+ # @overload new(point0, point1)
25
+ # Creates a {Obround} using the given {Point}s
26
+ # @param [Point] point0 A corner
27
+ # @param [Point] point1 The other corner
28
+ # @overload new(origin, size)
29
+ # Creates a {Obround} from the given origin and size
30
+ # @param [Point] origin Lower-left corner
31
+ # @param [Size] size Width and height
32
+ # @return [SizedObround]
33
+ # @overload new(left, bottom, right, top)
34
+ # Creates a {Obround} from the locations of each side
35
+ # @param [Number] left X-coordinate of the left side
36
+ # @param [Number] bottom Y-coordinate of the bottom edge
37
+ # @param [Number] right X-coordinate of the right side
38
+ # @param [Number] top Y-coordinate of the top edge
39
+ # @return [Obround]
40
+ def self.new(*args)
41
+ case args.size
42
+ when 1
43
+ CenteredObround.new(args[0])
44
+ when 2
45
+ if args.all? {|a| a.is_a?(Numeric) }
46
+ CenteredObround.new(Size[*args])
47
+ elsif args.all? {|a| a.is_a?(Array) || a.is_a?(Point) }
48
+ original_new(*args)
49
+ elsif (args[0].is_a?(Point) or args[0].is_a?(Array))and args[1].is_a?(Size)
50
+ SizedObround.new(*args)
51
+ else
52
+ raise ArgumentError, "Invalid arguments #{args}"
53
+ end
54
+ when 4
55
+ raise ArgumentError unless args.all? {|a| a.is_a?(Numeric)}
56
+ left, bottom, right, top = *args
57
+ original_new(Point[left, bottom], Point[right, top])
58
+ end
59
+ end
60
+
61
+ # Create a {Obround} using the given {Point}s
62
+ # @param [Point0] point0 The bottom-left corner (closest to the origin)
63
+ # @param [Point1] point1 The top-right corner (farthest from the origin)
64
+ def initialize(point0, point1)
65
+ point0, point1 = Point[point0], Point[point1]
66
+ raise(ArgumentError, "Point sizes must match") unless point0.size == point1.size
67
+
68
+ # Reorder the points to get lower-left and upper-right
69
+ if (point0.x > point1.x) && (point0.y > point1.y)
70
+ point0, point1 = point1, point0
71
+ else
72
+ p0x, p1x = [point0.x, point1.x].minmax
73
+ p0y, p1y = [point0.y, point1.y].minmax
74
+ point0 = Point[p0x, p0y]
75
+ point1 = Point[p1x, p1y]
76
+ end
77
+ @points = [point0, point1]
78
+ end
79
+
80
+ def eql?(other)
81
+ self.points == other.points
82
+ end
83
+ alias :== :eql?
84
+
85
+ # @group Accessors
86
+
87
+ # @return [Point] The {Obround}'s center
88
+ def center
89
+ min, max = @points.minmax {|a,b| a.y <=> b.y}
90
+ Point[(max.x+min.x)/2, (max.y+min.y)/2]
91
+ end
92
+
93
+ # @return [Array<Point>] The {Obround}'s four points (counterclockwise)
94
+ def points
95
+ point0, point2 = *@points
96
+ point1 = Point[point2.x, point0.y]
97
+ point3 = Point[point0.x, point2.y]
98
+ [point0, point1, point2, point3]
99
+ end
100
+
101
+ def origin
102
+ minx = @points.min {|a,b| a.x <=> b.x}
103
+ miny = @points.min {|a,b| a.y <=> b.y}
104
+ Point[minx.x, miny.y]
105
+ end
106
+
107
+ def height
108
+ min, max = @points.minmax {|a,b| a.y <=> b.y}
109
+ max.y - min.y
110
+ end
111
+
112
+ def width
113
+ min, max = @points.minmax {|a,b| a.x <=> b.x}
114
+ max.x - min.x
115
+ end
116
+ # @endgroup
117
+ end
118
+
119
+ class CenteredObround < Obround
120
+ # @return [Point] The {Obround}'s center
121
+ attr_accessor :center
122
+ attr_reader :origin
123
+ # @return [Size] The {Size} of the {Obround}
124
+ attr_accessor :size
125
+
126
+ # @overload new(width, height)
127
+ # Creates a {Obround} of the given width and height, centered on the origin
128
+ # @param [Number] height Height
129
+ # @param [Number] width Width
130
+ # @return [CenteredObround]
131
+ # @overload new(size)
132
+ # Creates a {Obround} of the given {Size} centered on the origin
133
+ # @param [Size] size Width and height
134
+ # @return [CenteredObround]
135
+ # @overload new(center, size)
136
+ # Creates a {Obround} with the given center point and size
137
+ # @param [Point] center
138
+ # @param [Size] size
139
+ def initialize(*args)
140
+ if args[0].is_a?(Size)
141
+ @center = Point[0,0]
142
+ @size = args[0]
143
+ elsif args[0].is_a?(Geometry::Point) and args[1].is_a?(Geometry::Size)
144
+ @center, @size = args[0,1]
145
+ elsif (2 == args.size) and args.all? {|a| a.is_a?(Numeric)}
146
+ @center = Point[0,0]
147
+ @size = Geometry::Size[*args]
148
+ end
149
+ end
150
+
151
+ def eql?(other)
152
+ (self.center == other.center) && (self.size == other.size)
153
+ end
154
+ alias :== :eql?
155
+
156
+ # @group Accessors
157
+ # @return [Array<Point>] The {Obround}'s four points (clockwise)
158
+ def points
159
+ point0 = @center - @size/2.0
160
+ point2 = @center + @size/2.0
161
+ point1 = Point[point0.x,point2.y]
162
+ point3 = Point[point2.x, point0.y]
163
+ [point0, point1, point2, point3]
164
+ end
165
+
166
+ def height
167
+ @size.height
168
+ end
169
+
170
+ def width
171
+ @size.width
172
+ end
173
+ # @endgroup
174
+ end
175
+
176
+ class SizedObround < Obround
177
+ # @return [Point] The {Obround}'s center
178
+ attr_reader :center
179
+ # @return [Point] The {Obround}'s origin
180
+ attr_accessor :origin
181
+ # @return [Size] The {Size} of the {Obround}
182
+ attr_accessor :size
183
+
184
+ # @overload new(width, height)
185
+ # Creates an {Obround} of the given width and height with its origin at [0,0]
186
+ # @param [Number] height Height
187
+ # @param [Number] width Width
188
+ # @return SizedObround
189
+ # @overload new(size)
190
+ # Creates an {Obround} of the given {Size} with its origin at [0,0]
191
+ # @param [Size] size Width and height
192
+ # @return SizedObround
193
+ # @overload new(origin, size)
194
+ # Creates an {Obround} with the given origin point and size
195
+ # @param [Point] origin
196
+ # @param [Size] size
197
+ # @return SizedObround
198
+ def initialize(*args)
199
+ if args[0].is_a?(Size)
200
+ @origin = Point[0,0]
201
+ @size = args[0]
202
+ elsif (args[0].is_a?(Point) or args[0].is_a?(Array)) and args[1].is_a?(Geometry::Size)
203
+ @origin, @size = Point[args[0]], args[1]
204
+ elsif (2 == args.size) and args.all? {|a| a.is_a?(Numeric)}
205
+ @origin = Point[0,0]
206
+ @size = Geometry::Size[*args]
207
+ end
208
+ end
209
+
210
+ def eql?(other)
211
+ (self.origin == other.origin) && (self.size == other.size)
212
+ end
213
+ alias :== :eql?
214
+
215
+ # @group Accessors
216
+ def center
217
+ @origin + @size/2
218
+ end
219
+
220
+ # @return [Array<Point>] The {Obround}'s four points (clockwise)
221
+ def points
222
+ point0 = @origin
223
+ point2 = @origin + @size
224
+ point1 = Point[point0.x,point2.y]
225
+ point3 = Point[point2.x, point0.y]
226
+ [point0, point1, point2, point3]
227
+ end
228
+
229
+ def height
230
+ @size.height
231
+ end
232
+
233
+ def width
234
+ @size.width
235
+ end
236
+ # @endgroup
237
+ end
238
+ end