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,67 @@
1
+ require 'geometry/arc'
2
+ require 'geometry/edge'
3
+
4
+ module Geometry
5
+ =begin
6
+ An object representing a set of connected elements, each of which could be an
7
+ {Edge} or an {Arc}. Unlike a {Polygon}, a {Path} is not guaranteed to be closed.
8
+ =end
9
+ class Path
10
+ attr_reader :elements
11
+
12
+ # Construct a new Path from {Point}s, {Edge}s, and {Arc}s
13
+ # Successive {Point}s will be converted to {Edge}s.
14
+ def initialize(*args)
15
+ args.map! {|a| (a.is_a?(Array) or a.is_a?(Vector)) ? Point[a] : a}
16
+ args.each {|a| raise ArgumentError, "Unknown argument type #{a.class}" unless a.is_a?(Point) or a.is_a?(Edge) or a.is_a?(Arc) }
17
+
18
+ @elements = []
19
+
20
+ first = args.shift
21
+ push first if first.is_a?(Edge) or first.is_a?(Arc)
22
+
23
+ args.reduce(first) do |previous, n|
24
+ case n
25
+ when Point
26
+ case previous
27
+ when Point then push Edge.new(previous, n)
28
+ when Arc, Edge then push Edge.new(previous.last, n) unless previous.last == n
29
+ end
30
+ last
31
+ when Edge
32
+ case previous
33
+ when Point then push Edge.new(previous, n.first)
34
+ when Arc, Edge then push Edge.new(previous.last, n.first) unless previous.last == n.first
35
+ end
36
+ push(n).last
37
+ when Arc
38
+ case previous
39
+ when Point
40
+ if previous == n.first
41
+ raise ArgumentError, "Duplicated point before an Arc"
42
+ else
43
+ push Edge.new(previous, n.first)
44
+ end
45
+ when Arc, Edge
46
+ push Edge.new(previous.last, n.first) unless previous.last == n.first
47
+ end
48
+ push(n).last
49
+ else
50
+ raise ArgumentError, "Unsupported argument type: #{n}"
51
+ end
52
+ end
53
+ end
54
+
55
+ # @return [Geometry] The last element in the {Path}
56
+ def last
57
+ @elements.last
58
+ end
59
+
60
+ # Append a new geometry element to the {Path}
61
+ # @return [Path]
62
+ def push(arg)
63
+ @elements.push arg
64
+ self
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,163 @@
1
+ require 'matrix'
2
+
3
+ require_relative 'point_zero'
4
+
5
+ module Geometry
6
+ DimensionMismatch = Class.new(StandardError)
7
+ OperationNotDefined = Class.new(StandardError)
8
+
9
+ =begin rdoc
10
+ An object repesenting a Point in N-dimensional space
11
+
12
+ Supports all of the familiar Vector methods and adds convenience
13
+ accessors for those variables you learned to hate in your high school
14
+ geometry class (x, y, z).
15
+
16
+ == Usage
17
+
18
+ === Constructor
19
+ point = Geometry::Point[x,y]
20
+ =end
21
+ class Point < Vector
22
+ attr_reader :x, :y, :z
23
+
24
+ # Allow vector-style initialization, but override to support copy-init
25
+ # from Vector or another Point
26
+ #
27
+ # @overload [](x,y,z,...)
28
+ # @overload [](Array)
29
+ # @overload [](Point)
30
+ # @overload [](Vector)
31
+ def self.[](*array)
32
+ return array[0] if array[0].is_a?(Point) or array[0].is_a?(PointZero)
33
+ array = array[0] if array[0].is_a?(Array)
34
+ array = array[0].to_a if array[0].is_a?(Vector)
35
+ super *array
36
+ end
37
+
38
+ # Creates and returns a new {PointZero} instance. Or, a {Point} full of zeros if the size argument is given.
39
+ # @param size [Number] the size of the new {Point} full of zeros
40
+ # @return [PointZero] A new {PointZero} instance
41
+ def self.zero(size=nil)
42
+ size ? Point[Array.new(size, 0)] : PointZero.new
43
+ end
44
+
45
+ # Return a copy of the {Point}
46
+ def clone
47
+ Point[@elements.clone]
48
+ end
49
+
50
+ # Allow comparison with an Array, otherwise do the normal thing
51
+ def eql?(other)
52
+ if other.is_a?(Array)
53
+ @elements.eql? other
54
+ elsif other.is_a?(PointZero)
55
+ @elements.all? {|e| e.eql? 0 }
56
+ else
57
+ super other
58
+ end
59
+ end
60
+
61
+ # Allow comparison with an Array, otherwise do the normal thing
62
+ def ==(other)
63
+ if other.is_a?(Array)
64
+ @elements.eql? other
65
+ elsif other.is_a?(PointZero)
66
+ @elements.all? {|e| e.eql? 0 }
67
+ else
68
+ super other
69
+ end
70
+ end
71
+
72
+ # Combined comparison operator
73
+ # @return [Point] The <=> operator is applied to the elements of the arguments pairwise and the results are returned in a Point
74
+ def <=>(other)
75
+ Point[self.to_a.zip(other.to_a).map {|a,b| a <=> b}.compact]
76
+ end
77
+
78
+ def coerce(other)
79
+ case other
80
+ when Array then [Point[*other], self]
81
+ when Numeric then [Point[Array.new(self.size, other)], self]
82
+ when Vector then [Point[*other], self]
83
+ else
84
+ raise TypeError, "#{self.class} can't be coerced into #{other.class}"
85
+ end
86
+ end
87
+
88
+ def inspect
89
+ 'Point' + @elements.inspect
90
+ end
91
+ def to_s
92
+ 'Point' + @elements.to_s
93
+ end
94
+
95
+ # @group Accessors
96
+ # @param [Integer] i Index into the {Point}'s elements
97
+ # @return [Numeric] Element i (starting at 0)
98
+ def [](i)
99
+ @elements[i]
100
+ end
101
+
102
+ # @attribute [r] x
103
+ # @return [Numeric] X-component
104
+ def x
105
+ @elements[0]
106
+ end
107
+
108
+ # @attribute [r] y
109
+ # @return [Numeric] Y-component
110
+ def y
111
+ @elements[1]
112
+ end
113
+
114
+ # @attribute [r] z
115
+ # @return [Numeric] Z-component
116
+ def z
117
+ @elements[2]
118
+ end
119
+ # @endgroup
120
+
121
+ # @group Arithmetic
122
+
123
+ # @group Unary operators
124
+ def +@
125
+ self
126
+ end
127
+
128
+ def -@
129
+ Point[@elements.map {|e| -e }]
130
+ end
131
+ # @endgroup
132
+
133
+ def +(other)
134
+ case other
135
+ when Numeric
136
+ Point[@elements.map {|e| e + other}]
137
+ when PointZero, NilClass
138
+ self.dup
139
+ else
140
+ raise OperationNotDefined, "#{other.class} must respond to :size and :[]" unless other.respond_to?(:size) && other.respond_to?(:[])
141
+ raise DimensionMismatch, "Can't add #{other} to #{self}" if size != other.size
142
+ Point[Array.new(size) {|i| @elements[i] + other[i] }]
143
+ end
144
+ end
145
+
146
+ def -(other)
147
+ case other
148
+ when Numeric
149
+ Point[@elements.map {|e| e - other}]
150
+ when PointZero, NilClass
151
+ self.dup
152
+ else
153
+ raise OperationNotDefined, "#{other.class} must respond to :size and :[]" unless other.respond_to?(:size) && other.respond_to?(:[])
154
+ raise DimensionMismatch, "Can't subtract #{other} from #{self}" if size != other.size
155
+ Point[Array.new(size) {|i| @elements[i] - other[i] }]
156
+ end
157
+ end
158
+
159
+ # @endgroup
160
+
161
+ end
162
+ end
163
+
@@ -0,0 +1,107 @@
1
+ require_relative 'point'
2
+
3
+ module Geometry
4
+ =begin rdoc
5
+ An object repesenting a {Point} at the origin in N-dimensional space
6
+
7
+ A {PointZero} object is a {Point} that will always compare equal to zero and unequal to
8
+ everything else, regardless of size. You can think of it as an application of the
9
+ {http://en.wikipedia.org/wiki/Null_Object_pattern Null Object Pattern}.
10
+ =end
11
+ class PointZero
12
+ def eql?(other)
13
+ if other.respond_to? :all?
14
+ other.all? {|e| e.eql? 0}
15
+ else
16
+ other == 0
17
+ end
18
+ end
19
+ alias == eql?
20
+
21
+ def coerce(other)
22
+ if other.is_a? Numeric
23
+ [other, 0]
24
+ elsif other.is_a? Array
25
+ [other, Array.new(other.size,0)]
26
+ elsif other.is_a? Vector
27
+ [other, Vector[*Array.new(other.size,0)]]
28
+ else
29
+ [Point[other], Point[Array.new(other.size,0)]]
30
+ end
31
+ end
32
+
33
+ # This is a hack to get Array#== to work properly. It works on ruby 2.0 and 1.9.3.
34
+ def to_ary
35
+ []
36
+ end
37
+
38
+ # @group Accessors
39
+ # @param [Integer] i Index into the {Point}'s elements
40
+ # @return [Numeric] Element i (starting at 0)
41
+ def [](i)
42
+ 0
43
+ end
44
+
45
+ # @attribute [r] x
46
+ # @return [Numeric] X-component
47
+ def x
48
+ 0
49
+ end
50
+
51
+ # @attribute [r] y
52
+ # @return [Numeric] Y-component
53
+ def y
54
+ 0
55
+ end
56
+
57
+ # @attribute [r] z
58
+ # @return [Numeric] Z-component
59
+ def z
60
+ 0
61
+ end
62
+ # @endgroup
63
+
64
+ # @group Arithmetic
65
+
66
+ # @group Unary operators
67
+ def +@
68
+ self
69
+ end
70
+
71
+ def -@
72
+ self
73
+ end
74
+ # @endgroup
75
+
76
+ def +(other)
77
+ case other
78
+ when Array, Numeric then other
79
+ else
80
+ Point[other]
81
+ end
82
+ end
83
+
84
+ def -(other)
85
+ if other.is_a? Size
86
+ -Point[other]
87
+ elsif other.respond_to? :-@
88
+ -other
89
+ elsif other.respond_to? :map
90
+ other.map {|a| -a }
91
+ end
92
+ end
93
+
94
+ def *(other)
95
+ self
96
+ end
97
+
98
+ def /(other)
99
+ raise OperationNotDefined unless other.is_a? Numeric
100
+ raise ZeroDivisionError if 0 == other
101
+ self
102
+ end
103
+ # @endgroup
104
+
105
+ end
106
+ end
107
+
@@ -0,0 +1,368 @@
1
+ require_relative 'edge'
2
+ require_relative 'polyline'
3
+
4
+ module Geometry
5
+
6
+ =begin rdoc
7
+ A {Polygon} is a closed path comprised entirely of lines so straight they don't even curve.
8
+
9
+ {http://en.wikipedia.org/wiki/Polygon}
10
+
11
+ The {Polygon} class is generally intended to represent {http://en.wikipedia.org/wiki/Simple_polygon Simple polygons},
12
+ but there's currently nothing that enforces simplicity.
13
+
14
+ == Usage
15
+
16
+ =end
17
+
18
+ class Polygon < Polyline
19
+
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.
26
+ # @overload initialize(Edge, Edge, ...)
27
+ # @return [Polygon]
28
+ # @overload initialize(Point, Point, ...)
29
+ # @return [Polygon]
30
+ def initialize(*args)
31
+ super
32
+ close! # A Polygon is always closed
33
+ end
34
+
35
+ # This method returns the receiver because a {Polygon} is always closed
36
+ # @return [Polygon] the receiver
37
+ def close
38
+ close!
39
+ end
40
+
41
+ # Check the orientation of the {Polygon}
42
+ # @return [Boolean] True if the {Polygon} is clockwise, otherwise false
43
+ def clockwise?
44
+ edges.map {|e| (e.last.x - e.first.x) * (e.last.y + e.first.y)}.reduce(:+) >= 0
45
+ end
46
+
47
+ # @return [Polygon] A new {Polygon} with orientation that's the opposite of the receiver
48
+ def reverse
49
+ self.class.new *(self.vertices.reverse)
50
+ end
51
+
52
+ # Reverse the receiver and return it
53
+ # @return [Polygon] the reversed receiver
54
+ def reverse!
55
+ super
56
+
57
+ # Simply reversing the vertex array causes the reversed polygon to
58
+ # start at what had been the last vertex, instead of starting at
59
+ # the same vertex and just going the other direction.
60
+ vertices.unshift vertices.pop
61
+
62
+ self
63
+ end
64
+
65
+ # @group Boolean operators
66
+
67
+ # Test a {Point} for inclusion in the receiver using a simplified winding number algorithm
68
+ # @param [Point] point The {Point} to test
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
+ def <=>(point)
71
+ sum = edges.reduce(0) do |sum, e|
72
+ direction = e.last.y <=> e.first.y
73
+ # Ignore edges that don't cross the point's x coordinate
74
+ next sum unless ((point.y <=> e.last.y) + (point.y <=> e.first.y)).abs <= 1
75
+
76
+ if 0 == direction # Special case horizontal edges
77
+ return 0 if ((point.x <=> e.last.x) + (point.x <=> e.first.x)).abs <= 1
78
+ next sum # Doesn't intersect
79
+ else
80
+ is_left = e <=> point
81
+ return 0 if 0 == is_left
82
+ next sum unless is_left
83
+ sum += 0 <=> (direction + is_left)
84
+ end
85
+ end
86
+ (0 == sum) ? -1 : 1
87
+ end
88
+
89
+ # Create a new {Polygon} that's the union of the receiver and a passed {Polygon}
90
+ # This is a simplified implementation of the alogrithm outlined in the
91
+ # paper {http://gvu.gatech.edu/people/official/jarek/graphics/papers/04PolygonBooleansMargalit.pdf An algorithm for computing the union, intersection or difference of two polygons}.
92
+ # In particular, this method assumes the receiver and passed {Polygon}s are "island" type and that the desired output is "regular", as those terms are described in the paper.
93
+ # @param [Polygon] other The {Polygon} to union with the receiver
94
+ # @return [Polygon] The union of the receiver and the passed {Polygon}
95
+ def union(other)
96
+ # Table 1: Both polygons are islands and the operation is union, so both must have the same orientation
97
+ # Reverse the other polygon if the orientations are different
98
+ other = other.reverse if self.clockwise? != other.clockwise?
99
+
100
+ # Receiver's vertex ring
101
+ ringA = VertexRing.new
102
+ self.vertices.each {|v| ringA.push v, (other <=> v)}
103
+
104
+ # The other vertex ring
105
+ ringB = VertexRing.new
106
+ other.vertices.each {|v| ringB.push v, (self <=> v)}
107
+
108
+ # Find intersections
109
+ offsetA = 0
110
+ edgesB = other.edges.dup
111
+ self.edges.each_with_index do |a, indexA|
112
+ offsetB = 0
113
+ ringB.edges_with_index do |b, indexB|
114
+ intersection = a.intersection(b)
115
+ if intersection === true
116
+ if (a.first == b.first) and (a.last == b.last) # Equal edges
117
+ elsif (a.first == b.last) and (a.last == b.first) # Ignore equal but opposite edges
118
+ else
119
+ if a.vector.normalize == b.vector.normalize # Same direction?
120
+ offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, b.first)
121
+ offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, a.last)
122
+ else # Opposite direction
123
+ offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, b.last)
124
+ offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, a.first)
125
+ end
126
+ end
127
+ elsif intersection.is_a?(Point)
128
+ offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, intersection)
129
+ offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, intersection)
130
+ end
131
+ end
132
+ end
133
+
134
+ # Table 2: Both polygons are islands and the operation is union, so select outside from both polygons
135
+ edgeFragments = []
136
+ [[ringA, other], [ringB, self]].each do |ring, other_polygon|
137
+ ring.edges do |v1,v2|
138
+ if (v1[:type] == -1) or (v2[:type] == -1)
139
+ edgeFragments.push :first => v1[:vertex], :last => v2[:vertex]
140
+ elsif (v1[:type] == 0) and (v2[:type] == 0)
141
+ if (other_polygon <=> Point[(v1[:vertex] + v2[:vertex])/2]) <= 0
142
+ edgeFragments.push :first => v1[:vertex], :last => v2[:vertex]
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ # Delete any duplicated edges. Array#uniq doesn't do the right thing, so using inject instead.
149
+ edgeFragments = edgeFragments.inject([]) {|result,h| result << h unless result.include?(h); result}
150
+
151
+ # Delete any equal-and-opposite edges
152
+ edgeFragments = edgeFragments.reject {|f| edgeFragments.find {|f2| (f[:first] == f2[:last]) and (f[:last] == f2[:first])} }
153
+
154
+ # Construct the output polygons
155
+ output = edgeFragments.reduce([Array.new]) do |output, fragment|
156
+ next output if fragment.empty?
157
+ polygon = output.last
158
+ polygon.push fragment[:first], fragment[:last] if polygon.empty?
159
+ while 1 do
160
+ adjacent_fragment = edgeFragments.find {|f| fragment[:last] == f[:first]}
161
+ break unless adjacent_fragment
162
+
163
+ polygon.push adjacent_fragment[:first], adjacent_fragment[:last]
164
+ fragment = adjacent_fragment.dup
165
+ adjacent_fragment.clear
166
+
167
+ break if polygon.first == polygon.last # closed?
168
+ end
169
+ output << Array.new
170
+ end
171
+
172
+ # If everything worked properly there should be only one output Polygon
173
+ output.reject! {|a| a.empty?}
174
+ output = Polygon.new *(output[0])
175
+
176
+ # Table 4: Both input polygons are "island" type and the operation
177
+ # is union, so the output polygon's orientation should be the same
178
+ # as the input polygon's orientation
179
+ (self.clockwise? != output.clockwise?) ? output.reverse : output
180
+ end
181
+ alias :+ :union
182
+
183
+ # @endgroup
184
+
185
+ # @group Convex Hull
186
+
187
+ # Returns the convex hull of the {Polygon}
188
+ # @return [Polygon] A convex {Polygon}, or the original {Polygon} if it's already convex
189
+ def convex
190
+ wrap
191
+ end
192
+
193
+ # Returns the convex hull using the {http://en.wikipedia.org/wiki/Gift_wrapping_algorithm Gift Wrapping algorithm}
194
+ # This implementation was cobbled together from many sources, but mostly from this implementation of the {http://butunclebob.com/ArticleS.UncleBob.ConvexHullTiming Jarvis March}
195
+ # @return [Polygon]
196
+ def wrap
197
+ # Start with a Point that's guaranteed to be on the hull
198
+ leftmost_point = vertices.min_by {|v| v.x}
199
+ current_point = vertices.select {|v| v.x == leftmost_point.x}.min_by {|v| v.y}
200
+
201
+ current_angle = 0.0
202
+ hull_points = [current_point]
203
+ while true
204
+ min_angle = 4.0
205
+ min_point = nil
206
+ vertices.each do |v1|
207
+ next if current_point.equal? v1
208
+ angle = pseudo_angle_for_edge(current_point, v1)
209
+ min_point, min_angle = v1, angle if (angle >= current_angle) && (angle <= min_angle)
210
+ end
211
+ current_angle = min_angle
212
+ current_point = min_point
213
+ break if current_point == hull_points.first
214
+ hull_points << min_point
215
+ end
216
+ Polygon.new *hull_points
217
+ end
218
+
219
+ # @endgroup
220
+
221
+ # Outset the receiver by the specified distance
222
+ # @param [Number] distance The distance to offset by
223
+ # @return [Polygon] A new {Polygon} outset by the given distance
224
+ def outset(distance)
225
+ bisector_edges = outset_bisectors(distance)
226
+ bisector_pairs = bisector_edges.push(bisector_edges.first).each_cons(2)
227
+
228
+ # Create the offset edges and then wrap them in Hashes so the edges
229
+ # can be altered while walking the array
230
+ active_edges = edges.zip(bisector_pairs).map do |e,offset|
231
+ offset_edge = Edge.new(e.first+offset.first.vector, e.last+offset.last.vector)
232
+
233
+ # Skip zero-length edges
234
+ {:edge => (offset_edge.first == offset_edge.last) ? nil : offset_edge}
235
+ end
236
+
237
+ # Walk the array and handle any intersections
238
+ active_edges.each_with_index do |e, i|
239
+ e1 = e[:edge]
240
+ next unless e1 # Ignore deleted edges
241
+
242
+ intersection, j = find_last_intersection(active_edges, i, e1)
243
+ if intersection
244
+ e2 = active_edges[j][:edge]
245
+ wrap_around_is_shortest = ((i + active_edges.count - j) < (j-i))
246
+
247
+ if intersection.is_a? Point
248
+ if wrap_around_is_shortest
249
+ active_edges[i][:edge] = Edge.new(intersection, e1.last)
250
+ active_edges[j][:edge] = Edge.new(e2.first, intersection)
251
+ else
252
+ active_edges[i][:edge] = Edge.new(e1.first, intersection)
253
+ active_edges[j][:edge] = Edge.new(intersection, e2.last)
254
+ end
255
+ else
256
+ # Handle the collinear case
257
+ active_edges[i][:edge] = Edge.new(e1.first, e2.last)
258
+ active_edges[j].delete(:edge)
259
+ wrap_around_is_shortest = false
260
+ end
261
+
262
+ # Delete everything between e1 and e2
263
+ if wrap_around_is_shortest # Choose the shortest path
264
+ for k in 0...i do
265
+ active_edges[k].delete(:edge)
266
+ end
267
+ for k in j...active_edges.count do
268
+ next if k==j # Exclude e2
269
+ active_edges[k].delete(:edge)
270
+ end
271
+ else
272
+ for k in i...j do
273
+ next if k==i # Exclude e1 and e2
274
+ active_edges[k].delete(:edge)
275
+ end
276
+ end
277
+
278
+ redo # Recheck the modified edges
279
+ end
280
+ end
281
+ Polygon.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten)
282
+ end
283
+
284
+ # Vertex bisectors suitable for outsetting
285
+ # @param [Number] length The distance to offset by
286
+ # @return [Array<Edge>] {Edge}s representing the bisectors
287
+ def outset_bisectors(length)
288
+ vertices.zip(spokes).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil}
289
+ end
290
+
291
+ # Generate the unit-length spokes for each vertex
292
+ # @return [Array<Vector>] the unit {Vector}s representing the spoke of each vertex
293
+ def spokes
294
+ clockwise? ? left_bisectors : right_bisectors
295
+ end
296
+
297
+ private
298
+
299
+ # Return a number that increases with the slope of the {Edge}
300
+ # @return [Number] A number in the range [0,4)
301
+ def pseudo_angle_for_edge(point0, point1)
302
+ delta = Point[point1.x.to_f, point1.y.to_f] - Point[point0.x.to_f, point0.y.to_f]
303
+ if delta.x >= 0
304
+ if delta.y >= 0
305
+ quadrant_one_psuedo_angle(delta.x, delta.y)
306
+ else
307
+ 1 + quadrant_one_psuedo_angle(delta.y.abs, delta.x)
308
+ end
309
+ else
310
+ if delta.y >= 0
311
+ 3 + quadrant_one_psuedo_angle(delta.y, delta.x.abs)
312
+ else
313
+ 2 + quadrant_one_psuedo_angle(delta.x.abs, delta.y.abs)
314
+ end
315
+ end
316
+ end
317
+
318
+ def quadrant_one_psuedo_angle(dx, dy)
319
+ dx / (dx + dy)
320
+ end
321
+ end
322
+
323
+ private
324
+
325
+ class VertexRing
326
+ attr_reader :vertices
327
+
328
+ def initialize
329
+ @vertices = []
330
+ end
331
+
332
+ # @param [Integer] index The index to insert the new {Point} before
333
+ # @param [Point] point The {Point} to insert
334
+ # @param [Integer] type The vertex type: 1 is inside, 0 is boundary, -1 is outside
335
+ def insert(index, point, type)
336
+ if v = @vertices.find {|v| v[:vertex] == point }
337
+ v[:type] = type
338
+ false
339
+ else
340
+ @vertices.insert(index, {:vertex => point, :type => type})
341
+ true
342
+ end
343
+ end
344
+
345
+ # Insert a boundary vertex
346
+ # @param [Integer] index The index to insert the new {Point} before
347
+ # @param [Point] point The {Point} to insert
348
+ def insert_boundary(index, point)
349
+ self.insert(index, point, 0)
350
+ end
351
+
352
+ # @param [Point] point The {Point} to push
353
+ # @param [Integer] type The vertex type: 1 is inside, 0 is boundary, -1 is outside
354
+ def push(point, type)
355
+ @vertices << {:vertex => point, :type => type}
356
+ end
357
+
358
+ # Enumerate the pairs of vertices corresponding to each edge
359
+ def edges
360
+ (@vertices + [@vertices.first]).each_cons(2) {|v1,v2| yield v1, v2}
361
+ end
362
+
363
+ def edges_with_index
364
+ index = 0
365
+ (@vertices + [@vertices.first]).each_cons(2) {|v1,v2| yield(Edge.new(v1[:vertex], v2[:vertex]), index); index += 1}
366
+ end
367
+ end
368
+ end