Class: Geometry::Polyline

Inherits:
Object
  • Object
show all
Defined in:
lib/geometry/polyline.rb

Overview

A Polyline is like a Polygon in that it only contains straight lines, but also like a Path in that it isn’t necessarily closed.

http://en.wikipedia.org/wiki/Polyline

Usage

Direct Known Subclasses

Polygon

Instance Attribute Summary collapse

Attributes collapse

Bisectors collapse

Instance Method Summary collapse

Constructor Details

#initialize(Edge, Edge, ...) ⇒ Polyline #initialize(Point, Point, ...) ⇒ Polyline

Note:

The constructor will try to convert all of its arguments into Geometry::Points and Edges. Then successive Geometry::Points will be collpased into Edges. Successive Edges that share a common vertex will be added to the new Geometry::Polyline. If there’s a gap between Edges it will be automatically filled with a new Edge.

Construct a new Polyline from Points and/or Edges

Overloads:

  • #initialize(Edge, Edge, ...) ⇒ Polyline
  • #initialize(Point, Point, ...) ⇒ Polyline


32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/geometry/polyline.rb', line 32

def initialize(*args)
    args.map! {|a| (a.is_a?(Array) || a.is_a?(Vector)) ? Point[a] : a}
    args.each {|a| raise ArgumentError, "Unknown argument type #{a.class}" unless a.is_a?(Point) or a.is_a?(Edge) }

    @edges = [];
    @vertices = [];

    first = args.shift
    if first.is_a?(Point)
	@vertices.push first
    elsif first.is_a?(Edge)
	@edges.push first
	@vertices.push *(first.to_a)
    end

    args.reduce(@vertices.last) do |previous,n|
	if n.is_a?(Point)
	    if n == previous	# Ignore repeated Points
		previous
	    else
		if @edges.last
		    new_edge = Edge.new(previous, n)
		    if @edges.last.parallel?(new_edge)
			popped_edge = pop_edge		# Remove the previous Edge
			if n == popped_edge.first
			    popped_edge.first
			else
			    push_edge Edge.new(popped_edge.first, n)
			    push_vertex popped_edge.first
			    push_vertex n
			    n
			end
		    else
			push_edge Edge.new(previous, n)
			push_vertex n
			n
		    end
		else
		    push_edge Edge.new(previous, n)
		    push_vertex n
		    n
		end
	    end
	elsif n.is_a?(Edge)
	    if previous == n.first
		push_edge n
		push_vertex n.last
	    elsif previous == n.last
		push_edge n.reverse!
		push_vertex n.last
	    else
		e = Edge.new(previous, n.first)
		push_edge e, n
		push_vertex *(e.to_a), *(n.to_a)
	    end
	    n.last
	end
    end
end

Instance Attribute Details

#edgesObject (readonly)

Returns the value of attribute edges.



16
17
18
# File 'lib/geometry/polyline.rb', line 16

def edges
  @edges
end

#points=(value) ⇒ Array<Point>

Returns all of the vertices of the Geometry::Polyline (alias of #vertices).

Returns:



20
21
22
# File 'lib/geometry/polyline.rb', line 20

def vertices
  @vertices
end

#verticesObject (readonly) Also known as: points

Returns the value of attribute vertices.



16
17
18
# File 'lib/geometry/polyline.rb', line 16

def vertices
  @vertices
end

Instance Method Details

#bisectorsArray<Vector>

Note:

If the Geometry::Polyline isn’t closed (the normal case), then the first and last vertices will be given bisectors that are perpendicular to themselves.

Generate the angle bisector unit vectors for each vertex

Returns:

  • (Array<Vector>)

    the unit Vectors representing the angle bisector of each vertex



187
188
189
190
# File 'lib/geometry/polyline.rb', line 187

def bisectors
    # Multiplying each bisector by the sign of k flips any bisectors that aren't pointing towards the interior of the angle
    bisector_map {|b, k| k <=> 0 }
end

#closePolyline

Clone the receiver, close it, then return it

Returns:

  • (Polyline)

    the closed clone of the receiver



121
122
123
# File 'lib/geometry/polyline.rb', line 121

def close
    clone.close!
end

#close!Polyline

Close the receiver and return it

Returns:

  • (Polyline)

    the receiver after closing



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/geometry/polyline.rb', line 127

def close!
    unless @edges.empty?
	# NOTE: parallel? is use here instead of collinear? because the
	#	edges are connected, and will therefore be collinear if
	#	they're parallel

	if closed?
	    if @edges.first.parallel?(@edges.last)
		unshift_edge Edge.new(@edges.last.first, shift_edge.last)
	    end
	elsif
	    closing_edge = Edge.new(@edges.last.last, @edges.first.first)

	    # If the closing edge is collinear with the last edge, then
	    #  simply extened the last edge to fill the gap
	    if @edges.last.parallel?(closing_edge)
		closing_edge = Edge.new(pop_edge.first, @edges.first.first)
	    end

	    # Check that the new closing_edge isn't zero-length
	    if closing_edge.first != closing_edge.last
		# If the closing edge is collinear with the first edge, then
		#  extend the first edge "backwards" to fill the gap
		if @edges.first.parallel?(closing_edge)
		    unshift_edge Edge.new(closing_edge.first, shift_edge.last)
		else
		    push_edge closing_edge
		end
	    end
	end
    end
    self
end

#closed?Bool

Check to see if the Geometry::Polyline is closed (ie. is it a Geometry::Polygon?)

Returns:

  • (Bool)

    true if the Geometry::Polyline is closed (the first vertex is equal to the last vertex)



163
164
165
# File 'lib/geometry/polyline.rb', line 163

def closed?
    @edges.last.last == @edges.first.first
end

#eql?(other) ⇒ Bool Also known as: ==

Check the equality of two Geometry::Polylines. Note that if two Geometry::Polylines have

opposite winding, but are otherwise identical, they will be considered unequal.

Returns:



95
96
97
# File 'lib/geometry/polyline.rb', line 95

def eql?(other)
    @vertices.zip(other.vertices).all? {|a,b| a == b}
end

#left_bisectorsArray<Vector>

Note:

This is similar to the #bisector method, but generates vectors that always point to the left side of the Geometry::Polyline instead of towards the inside of each corner

Generate left-side angle bisector unit vectors for each vertex

Returns:

  • (Array<Vector>)

    the unit Vectors representing the left-side angle bisector of each vertex



195
196
197
# File 'lib/geometry/polyline.rb', line 195

def left_bisectors
    bisector_map
end

#maxPoint

Returns The upper-right corner of the bounding rectangle that encloses the Geometry::Polyline.

Returns:



103
104
105
# File 'lib/geometry/polyline.rb', line 103

def max
    vertices.reduce {|memo, vertex| Point[[memo.x, vertex.x].max, [memo.y, vertex.y].max] }
end

#minPoint

Returns The lower-left corner of the bounding rectangle that encloses the Geometry::Polyline.

Returns:



108
109
110
# File 'lib/geometry/polyline.rb', line 108

def min
    vertices.reduce {|memo, vertex| Point[[memo.x, vertex.x].min, [memo.y, vertex.y].min] }
end

#minmaxArray<Point>

Returns The lower-left and upper-right corners of the enclosing bounding rectangle.

Returns:

  • (Array<Point>)

    The lower-left and upper-right corners of the enclosing bounding rectangle



113
114
115
# File 'lib/geometry/polyline.rb', line 113

def minmax
    vertices.reduce([vertices.first, vertices.first]) {|memo, vertex| [Point[[memo.first.x, vertex.x].min, [memo.first.y, vertex.y].min], Point[[memo.last.x, vertex.x].max, [memo.last.y, vertex.y].max]] }
end

#offset(distance) ⇒ Polyline Also known as: leftset

Note:

A positive distance will offset to the left, and a negative distance to the right.

Offset the receiver by the specified distance

Parameters:

  • distance (Number)

    The distance to offset by

Returns:



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/geometry/polyline.rb', line 221

def offset(distance)
    bisector_pairs = if closed?
	bisector_edges = offset_bisectors(distance)
	bisector_edges.push(bisector_edges.first).each_cons(2)
    else
	offset_bisectors(distance).each_cons(2)
    end

    # Create the offset edges and then wrap them in Hashes so the edges
    #  can be altered while walking the array
    active_edges = edges.zip(bisector_pairs).map do |e,offset|
	offset_edge = Edge.new(e.first+offset.first.vector, e.last+offset.last.vector)

	# Skip zero-length edges
	{:edge => (offset_edge.first == offset_edge.last) ? nil : offset_edge}
    end

    # Walk the array and handle any intersections
    for i in 0..(active_edges.count-1) do
	e1 = active_edges[i][:edge]
	next unless e1	# Ignore deleted edges

	intersection, j = find_last_intersection(active_edges, i, e1)
	if intersection
	    e2 = active_edges[j][:edge]
	    if intersection.is_a? Point
		active_edges[i][:edge] = Edge.new(e1.first, intersection)
		active_edges[j][:edge] = Edge.new(intersection, e2.last)
	    else
		# Handle the collinear case
		active_edges[i][:edge] = Edge.new(e1.first, e2.last)
		active_edges[j].delete(:edge)
	    end

	    # Delete everything between e1 and e2
	    for k in i..j do
		next if (k==i) or (k==j)    # Exclude e1 and e2
		active_edges[k].delete(:edge)
	    end

	    redo    # Recheck the modified edges
	end
    end
    Polyline.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten)
end

#reversePolyline

Clone the receiver, reverse it, then return it

Returns:



169
170
171
# File 'lib/geometry/polyline.rb', line 169

def reverse
    self.class.new *(edges.reverse.map! {|edge| edge.reverse! })
end

#reverse!Polyline

Reverse the receiver and return it

Returns:



175
176
177
178
179
# File 'lib/geometry/polyline.rb', line 175

def reverse!
    vertices.reverse!
    edges.reverse!.map! {|edge| edge.reverse! }
    self
end

#right_bisectorsArray<Vector>

Note:

This is similar to the #bisector method, but generates vectors that always point to the right side of the Geometry::Polyline instead of towards the inside of each corner

Generate right-side angle bisector unit vectors for each vertex

Returns:

  • (Array<Vector>)

    the unit Vectors representing the ride-side angle bisector of each vertex



202
203
204
# File 'lib/geometry/polyline.rb', line 202

def right_bisectors
    bisector_map {|b, k| -1 }
end

#rightset(distance) ⇒ Polyline

Rightset the receiver by the specified distance

Parameters:

  • distance (Number)

    The distance to offset by

Returns:



271
272
273
# File 'lib/geometry/polyline.rb', line 271

def rightset(distance)
    offset(-distance)
end

#spokesArray<Vector>

Note:

If the Geometry::Polyline isn’t closed (the normal case), then the first and last vertices will be given bisectors that are perpendicular to themselves.

Generate the spokes for each vertex. A spoke is the same as a bisector, but in the oppostire direction (bisectors point towards the inside of each corner; spokes point towards the outside)

Returns:

  • (Array<Vector>)

    the unit Vectors representing the spoke of each vertex



210
211
212
213
# File 'lib/geometry/polyline.rb', line 210

def spokes
    # Multiplying each bisector by the negated sign of k flips any bisectors that aren't pointing towards the exterior of the angle
    bisector_map {|b, k| 0 <=> k }
end