geometry 4 → 5
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.
- data/.gitignore +0 -3
- data/README.markdown +4 -2
- data/Rakefile +0 -8
- data/geometry.gemspec +1 -1
- data/lib/geometry.rb +3 -0
- data/lib/geometry/arc.rb +59 -0
- data/lib/geometry/circle.rb +72 -6
- data/lib/geometry/cluster_factory.rb +15 -0
- data/lib/geometry/edge.rb +61 -0
- data/lib/geometry/path.rb +55 -0
- data/lib/geometry/point.rb +21 -10
- data/lib/geometry/point_zero.rb +4 -4
- data/lib/geometry/polygon.rb +145 -46
- data/lib/geometry/polyline.rb +212 -0
- data/lib/geometry/rectangle.rb +3 -12
- data/lib/geometry/rotation.rb +29 -2
- data/lib/geometry/size_zero.rb +4 -4
- data/lib/geometry/square.rb +7 -7
- data/lib/geometry/triangle.rb +78 -0
- data/lib/geometry/vector.rb +18 -2
- data/test/geometry/arc.rb +9 -0
- data/test/geometry/circle.rb +49 -13
- data/test/geometry/edge.rb +53 -0
- data/test/geometry/path.rb +67 -0
- data/test/geometry/point.rb +27 -2
- data/test/geometry/polygon.rb +101 -0
- data/test/geometry/polyline.rb +91 -0
- data/test/geometry/rotation.rb +5 -0
- data/test/geometry/triangle.rb +32 -0
- data/test/geometry/vector.rb +6 -0
- metadata +12 -3
data/lib/geometry/point_zero.rb
CHANGED
@@ -29,9 +29,9 @@ everything else, regardless of size.
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
#
|
32
|
+
# @group Arithmetic
|
33
33
|
|
34
|
-
#
|
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
|
-
#
|
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
|
-
#
|
65
|
+
# @endgroup
|
66
66
|
|
67
67
|
end
|
68
68
|
end
|
data/lib/geometry/polygon.rb
CHANGED
@@ -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
|
-
|
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
|
23
|
+
# @overload initialize(Edge, Edge, ...)
|
25
24
|
# @return [Polygon]
|
26
|
-
# @overload
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
@edges
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
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
|
-
|
69
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
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
|