geometry 4 → 5

Sign up to get free protection for your applications and to get access to all the features.
@@ -29,9 +29,9 @@ everything else, regardless of size.
29
29
  end
30
30
  end
31
31
 
32
- # !@group Arithmetic
32
+ # @group Arithmetic
33
33
 
34
- # !@group Unary operators
34
+ # @group Unary operators
35
35
  def +@
36
36
  self
37
37
  end
@@ -39,7 +39,7 @@ everything else, regardless of size.
39
39
  def -@
40
40
  self
41
41
  end
42
- # !@endgroup
42
+ # @endgroup
43
43
 
44
44
  def +(other)
45
45
  other
@@ -62,7 +62,7 @@ everything else, regardless of size.
62
62
  raise ZeroDivisionError if 0 == other
63
63
  self
64
64
  end
65
- # !@endgroup
65
+ # @endgroup
66
66
 
67
67
  end
68
68
  end
@@ -1,9 +1,10 @@
1
1
  require_relative 'edge'
2
+ require_relative 'polyline'
2
3
 
3
4
  module Geometry
4
5
 
5
6
  =begin rdoc
6
- An object representing a closed set of vertices and edges.
7
+ A {Polygon} is a closed path comprised entirely of lines so straight they don't even curve.
7
8
 
8
9
  {http://en.wikipedia.org/wiki/Polygon}
9
10
 
@@ -11,9 +12,7 @@ An object representing a closed set of vertices and edges.
11
12
 
12
13
  =end
13
14
 
14
- class Polygon
15
- attr_reader :edges, :vertices
16
- alias :points :vertices
15
+ class Polygon < Polyline
17
16
 
18
17
  # Construct a new Polygon from Points and/or Edges
19
18
  # The constructor will try to convert all of its arguments into Points and
@@ -21,63 +20,163 @@ An object representing a closed set of vertices and edges.
21
20
  # Edges that share a common vertex will be added to the new Polygon. If
22
21
  # there's a gap between Edges it will be automatically filled with a new
23
22
  # Edge. The resulting Polygon will then be closed if it isn't already.
24
- # @overload new(Array, Array, ...)
23
+ # @overload initialize(Edge, Edge, ...)
25
24
  # @return [Polygon]
26
- # @overload new(Edge, Edge, ...)
27
- # @return [Polygon]
28
- # @overload new(Point, Point, ...)
29
- # @return [Polygon]
30
- # @overload new(Vector, Vector, ...)
25
+ # @overload initialize(Point, Point, ...)
31
26
  # @return [Polygon]
32
27
  def initialize(*args)
33
- args.map! {|a| (a.is_a?(Array) || a.is_a?(Vector)) ? Point[a] : a}
34
- raise(ArgumentError,'Unknown argument type') unless args.all? {|a| a.is_a?(Point) || a.is_a?(Edge) }
35
-
36
- @edges = [];
37
- @vertices = [];
38
-
39
- first = args.shift
40
- if first.is_a?(Point)
41
- @vertices.push first
42
- elsif first.is_a?(Edge)
43
- @edges.push first
44
- @vertices.push *(first.to_a)
28
+ super
29
+
30
+ # Close the polygon if needed
31
+ @edges.push Edge.new(@edges.last.last, @edges.first.first) unless @edges.empty? || (@edges.last.last == @edges.first.first)
32
+ end
33
+
34
+ # @group Convex Hull
35
+
36
+ # Returns the convex hull of the {Polygon}
37
+ # @return [Polygon] A convex {Polygon}, or the original {Polygon} if it's already convex
38
+ def convex
39
+ wrap
40
+ end
41
+
42
+ # Returns the convex hull using the {http://en.wikipedia.org/wiki/Gift_wrapping_algorithm Gift Wrapping algorithm}
43
+ # This implementation was cobbled together from many sources, but mostly from this implementation of the {http://butunclebob.com/ArticleS.UncleBob.ConvexHullTiming Jarvis March}
44
+ # @return [Polygon]
45
+ def wrap
46
+ # Start with a Point that's guaranteed to be on the hull
47
+ leftmost_point = vertices.min_by {|v| v.x}
48
+ current_point = vertices.select {|v| v.x == leftmost_point.x}.min_by {|v| v.y}
49
+
50
+ current_angle = 0.0
51
+ hull_points = [current_point]
52
+ while true
53
+ min_angle = 4.0
54
+ min_point = nil
55
+ vertices.each do |v1|
56
+ next if current_point.equal? v1
57
+ angle = pseudo_angle_for_edge(current_point, v1)
58
+ min_point, min_angle = v1, angle if (angle >= current_angle) && (angle <= min_angle)
59
+ end
60
+ current_angle = min_angle
61
+ current_point = min_point
62
+ break if current_point == hull_points.first
63
+ hull_points << min_point
64
+ end
65
+ Polygon.new *hull_points
66
+ end
67
+
68
+ # @endgroup
69
+
70
+ # Outset the receiver by the specified distance
71
+ # @param [Number] distance The distance to offset by
72
+ # @return [Polygon] A new {Polygon} outset by the given distance
73
+ def outset(distance)
74
+ bisectors = offset_bisectors(distance)
75
+ offsets = (bisectors.each_cons(2).to_a << [bisectors.last, bisectors.first])
76
+
77
+ # Create the offset edges and then wrap them in Hashes so the edges
78
+ # can be altered while walking the array
79
+ active_edges = edges.zip(offsets).map do |e,offset|
80
+ offset = Edge.new(e.first+offset.first.vector, e.last+offset.last.vector)
81
+
82
+ # Skip zero-length edges
83
+ {:edge => (offset.first == offset.last) ? nil : offset}
45
84
  end
46
85
 
47
- args.reduce(@vertices.last) do |previous,n|
48
- if n.is_a?(Point)
49
- push_edge Edge.new(previous, n)
50
- push_vertex n
51
- n
52
- elsif n.is_a?(Edge)
53
- if previous == n.first
54
- push_edge n
55
- push_vertex n.last
56
- elsif previous == n.last
57
- push_edge n.reverse!
58
- push_vertex n.last
86
+ # Walk the array and handle any intersections
87
+ active_edges.each_with_index do |e, i|
88
+ e1 = e[:edge]
89
+ next unless e1 # Ignore deleted edges
90
+
91
+ intersection, j = find_last_intersection(active_edges, i, e1)
92
+ if intersection
93
+ e2 = active_edges[j][:edge]
94
+ wrap_around_is_shortest = ((i + active_edges.count - j) < (j-i))
95
+
96
+ if intersection.is_a? Point
97
+ if wrap_around_is_shortest
98
+ active_edges[i][:edge] = Edge.new(intersection, e1.last)
99
+ active_edges[j][:edge] = Edge.new(e2.first, intersection)
100
+ else
101
+ active_edges[i][:edge] = Edge.new(e1.first, intersection)
102
+ active_edges[j][:edge] = Edge.new(intersection, e2.last)
103
+ end
59
104
  else
60
- e = Edge.new(previous, n.first)
61
- push_edge e, n
62
- push_vertex *(e.to_a), *(n.to_a)
105
+ # Handle the collinear case
106
+ active_edges[i][:edge] = Edge.new(e1.first, e2.last)
107
+ active_edges[j].delete(:edge)
63
108
  end
64
- n.last
109
+
110
+ # Delete everything between e1 and e2
111
+ if wrap_around_is_shortest # Choose the shortest path
112
+ for k in 0...i do
113
+ active_edges[k].delete(:edge)
114
+ end
115
+ for k in j...active_edges.count do
116
+ next if k==j # Exclude e2
117
+ active_edges[k].delete(:edge)
118
+ end
119
+ else
120
+ for k in i...j do
121
+ next if k==i # Exclude e1 and e2
122
+ active_edges[k].delete(:edge)
123
+ end
124
+ end
125
+
126
+ redo # Recheck the modified edges
65
127
  end
66
128
  end
129
+ Polygon.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten)
130
+ end
67
131
 
68
- # Close the polygon if needed
69
- @edges.push Edge.new(@edges.last.last, @edges.first.first) unless @edges.empty? || (@edges.last.last == @edges.first.first)
132
+ # Vertex bisectors suitable for offsetting
133
+ # @param [Number] length The distance to offset by
134
+ # @return [Array<Edge>] {Edge}s representing the bisectors
135
+ def offset_bisectors(length)
136
+ vectors = edges.map {|e| e.direction }
137
+ winding = 0
138
+ sums = vectors.unshift(vectors.last).each_cons(2).map do |v1,v2|
139
+ k = v1[0]*v2[1] - v1[1]*v2[0] # z-component of v1 x v2
140
+ winding += k
141
+ if v1 == v2 # collinear, same direction?
142
+ Vector[-v1[1], v1[0]]
143
+ elsif 0 == k # collinear, reverse direction
144
+ nil
145
+ else
146
+ by = (v2[1] - v1[1])/k
147
+ v = (0 == v1[1]) ? v2 : v1
148
+ Vector[(v[0]*by - 1)/v[1], by]
149
+ end
150
+ end
151
+
152
+ # Check the polygon's orientation. If clockwise, negate length as a hack for injecting a -1 into the final result
153
+ length = -length if winding >= 0
154
+ vertices.zip(sums).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil}
70
155
  end
71
156
 
72
157
  private
73
158
 
74
- def push_edge(*e)
75
- @edges.push *e
76
- @edges.uniq!
159
+ # Return a number that increases with the slope of the {Edge}
160
+ # @return [Number] A number in the range [0,4)
161
+ def pseudo_angle_for_edge(point0, point1)
162
+ delta = Point[point1.x.to_f, point1.y.to_f] - Point[point0.x.to_f, point0.y.to_f]
163
+ if delta.x >= 0
164
+ if delta.y >= 0
165
+ quadrant_one_psuedo_angle(delta.x, delta.y)
166
+ else
167
+ 1 + quadrant_one_psuedo_angle(delta.y.abs, delta.x)
168
+ end
169
+ else
170
+ if delta.y >= 0
171
+ 3 + quadrant_one_psuedo_angle(delta.y, delta.x.abs)
172
+ else
173
+ 2 + quadrant_one_psuedo_angle(delta.x.abs, delta.y.abs)
174
+ end
175
+ end
77
176
  end
78
- def push_vertex(*v)
79
- @vertices.push *v
80
- @vertices.uniq!
177
+
178
+ def quadrant_one_psuedo_angle(dx, dy)
179
+ dx / (dx + dy)
81
180
  end
82
181
  end
83
182
  end
@@ -0,0 +1,212 @@
1
+ require_relative 'edge'
2
+
3
+ module Geometry
4
+
5
+ =begin rdoc
6
+ A {Polyline} is like a {Polygon} in that it only contains straight lines, but
7
+ also like a {Path} in that it isn't necessarily closed.
8
+
9
+ {http://en.wikipedia.org/wiki/Polyline}
10
+
11
+ == Usage
12
+
13
+ =end
14
+
15
+ class Polyline
16
+ attr_reader :edges, :vertices
17
+
18
+ # Construct a new Polyline from Points and/or Edges
19
+ # The constructor will try to convert all of its arguments into {Point}s and
20
+ # {Edge}s. Then successive {Point}s will be collpased into {Edge}s. Successive
21
+ # {Edge}s that share a common vertex will be added to the new {Polyline}. If
22
+ # there's a gap between {Edge}s it will be automatically filled with a new
23
+ # {Edge}.
24
+ # @overload initialize(Edge, Edge, ...)
25
+ # @return [Polyline]
26
+ # @overload initialize(Point, Point, ...)
27
+ # @return [Polyline]
28
+ def initialize(*args)
29
+ args.map! {|a| (a.is_a?(Array) || a.is_a?(Vector)) ? Point[a] : a}
30
+ args.each {|a| raise ArgumentError, "Unknown argument type #{a.class}" unless a.is_a?(Point) or a.is_a?(Edge) }
31
+
32
+ @edges = [];
33
+ @vertices = [];
34
+
35
+ first = args.shift
36
+ if first.is_a?(Point)
37
+ @vertices.push first
38
+ elsif first.is_a?(Edge)
39
+ @edges.push first
40
+ @vertices.push *(first.to_a)
41
+ end
42
+
43
+ args.reduce(@vertices.last) do |previous,n|
44
+ if n.is_a?(Point)
45
+ if n == previous # Ignore repeated Points
46
+ previous
47
+ else
48
+ if @edges.last
49
+ new_edge = Edge.new(previous, n)
50
+ if @edges.last.parallel?(new_edge)
51
+ @edges.pop # Remove the previous Edge
52
+ @vertices.pop(@edges.size ? 1 : 2) # Remove the now unused vertex, or vertices
53
+ if n == @edges.last.last
54
+ @edges.last.last
55
+ else
56
+ push_edge Edge.new(@edges.last.last, n)
57
+ push_vertex @edges.last.first
58
+ push_vertex n
59
+ n
60
+ end
61
+ else
62
+ push_edge Edge.new(previous, n)
63
+ push_vertex n
64
+ n
65
+ end
66
+ else
67
+ push_edge Edge.new(previous, n)
68
+ push_vertex n
69
+ n
70
+ end
71
+ end
72
+ elsif n.is_a?(Edge)
73
+ if previous == n.first
74
+ push_edge n
75
+ push_vertex n.last
76
+ elsif previous == n.last
77
+ push_edge n.reverse!
78
+ push_vertex n.last
79
+ else
80
+ e = Edge.new(previous, n.first)
81
+ push_edge e, n
82
+ push_vertex *(e.to_a), *(n.to_a)
83
+ end
84
+ n.last
85
+ end
86
+ end
87
+ end
88
+
89
+ # Check the equality of two {Polyline}s. Note that if two {Polyline}s have
90
+ # opposite winding, but are otherwise identical, they will be considered unequal.
91
+ # @return [Bool] true if both {Polyline}s have equal edges
92
+ def eql?(other)
93
+ @vertices.zip(other.vertices).all? {|a,b| a == b}
94
+ end
95
+ alias :== :eql?
96
+
97
+ # Offset the receiver by the specified distance. A positive distance
98
+ # will offset to the left, and a negative distance to the right.
99
+ # @param [Number] distance The distance to offset by
100
+ # @return [Polygon] A new {Polygon} outset by the given distance
101
+ def offset(distance)
102
+ bisectors = offset_bisectors(distance)
103
+ offsets = bisectors.each_cons(2).to_a
104
+
105
+ # Create the offset edges and then wrap them in Hashes so the edges
106
+ # can be altered while walking the array
107
+ active_edges = edges.zip(offsets).map do |e,offset|
108
+ offset = Edge.new(e.first+offset.first.vector, e.last+offset.last.vector)
109
+
110
+ # Skip zero-length edges
111
+ {:edge => (offset.first == offset.last) ? nil : offset}
112
+ end
113
+
114
+ # Walk the array and handle any intersections
115
+ for i in 0..(active_edges.count-1) do
116
+ e1 = active_edges[i][:edge]
117
+ next unless e1 # Ignore deleted edges
118
+
119
+ intersection, j = find_last_intersection(active_edges, i, e1)
120
+ if intersection
121
+ e2 = active_edges[j][:edge]
122
+ if intersection.is_a? Point
123
+ active_edges[i][:edge] = Edge.new(e1.first, intersection)
124
+ active_edges[j][:edge] = Edge.new(intersection, e2.last)
125
+ else
126
+ # Handle the collinear case
127
+ active_edges[i][:edge] = Edge.new(e1.first, e2.last)
128
+ active_edges[j].delete(:edge)
129
+ end
130
+
131
+ # Delete everything between e1 and e2
132
+ for k in i..j do
133
+ next if (k==i) or (k==j) # Exclude e1 and e2
134
+ active_edges[k].delete(:edge)
135
+ end
136
+
137
+ redo # Recheck the modified edges
138
+ end
139
+ end
140
+ Polyline.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten)
141
+ end
142
+ alias :leftset :offset
143
+
144
+ # Rightset the receiver by the specified distance
145
+ # @param [Number] distance The distance to offset by
146
+ # @return [Polygon] A new {Polygon} rightset by the given distance
147
+ def rightset(distance)
148
+ offset(-distance)
149
+ end
150
+
151
+ private
152
+
153
+ # @group Helpers for offset()
154
+
155
+ # Vertex bisectors suitable for offsetting
156
+ # @param [Number] length The distance to offset by
157
+ # @return [Array<Edge>] {Edge}s representing the bisectors
158
+ def offset_bisectors(length)
159
+ vectors = edges.map {|e| e.direction }
160
+ winding = 0
161
+ sums = vectors.unshift(vectors.first).push(vectors.last).each_cons(2).map do |v1,v2|
162
+ k = v1[0]*v2[1] - v1[1]*v2[0] # z-component of v1 x v2
163
+ winding += k
164
+ if v1 == v2 # collinear, same direction?
165
+ Vector[-v1[1], v1[0]]
166
+ elsif 0 == k # collinear, reverse direction
167
+ nil
168
+ else
169
+ by = (v2[1] - v1[1])/k
170
+ v = (0 == v1[1]) ? v2 : v1
171
+ Vector[(v[0]*by - 1)/v[1], by]
172
+ end
173
+ end
174
+
175
+ vertices.zip(sums).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil}
176
+ end
177
+
178
+ # Find the next edge that intersects with e, starting at index i
179
+ def find_next_intersection(edges, i, e)
180
+ for j in i..(edges.count-1)
181
+ e2 = edges[j][:edge]
182
+ next if !e2 || e.connected?(e2)
183
+ intersection = e.intersection(e2)
184
+ return [intersection, j] if intersection
185
+ end
186
+ nil
187
+ end
188
+
189
+ # Find the last edge that intersects with e, starting at index i
190
+ def find_last_intersection(edges, i, e)
191
+ intersection, intersection_at = nil, nil
192
+ for j in i..(edges.count-1)
193
+ e2 = edges[j][:edge]
194
+ next if !e2 || e.connected?(e2)
195
+ _intersection = e.intersection(e2)
196
+ intersection, intersection_at = _intersection, j if _intersection
197
+ end
198
+ [intersection, intersection_at]
199
+ end
200
+ # @endgroup
201
+
202
+ def push_edge(*e)
203
+ @edges.push *e
204
+ @edges.uniq!
205
+ end
206
+
207
+ def push_vertex(*v)
208
+ @vertices.push *v
209
+ @vertices.uniq!
210
+ end
211
+ end
212
+ end