aurora-geometry 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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