require_relative 'edge' require_relative 'polyline' module Geometry =begin rdoc A {Polygon} is a closed path comprised entirely of lines so straight they don't even curve. {http://en.wikipedia.org/wiki/Polygon} The {Polygon} class is generally intended to represent {http://en.wikipedia.org/wiki/Simple_polygon Simple polygons}, but there's currently nothing that enforces simplicity. == Usage =end class Polygon < Polyline # Construct a new Polygon from Points and/or Edges # The constructor will try to convert all of its arguments into Points and # Edges. Then successive Points will be collpased into Edges. Successive # Edges that share a common vertex will be added to the new Polygon. If # there's a gap between Edges it will be automatically filled with a new # Edge. The resulting Polygon will then be closed if it isn't already. # @overload initialize(Edge, Edge, ...) # @return [Polygon] # @overload initialize(Point, Point, ...) # @return [Polygon] def initialize(*args) super close! # A Polygon is always closed end # This method returns the receiver because a {Polygon} is always closed # @return [Polygon] the receiver def close close! end # Check the orientation of the {Polygon} # @return [Boolean] True if the {Polygon} is clockwise, otherwise false def clockwise? edges.map {|e| (e.last.x - e.first.x) * (e.last.y + e.first.y)}.reduce(:+) >= 0 end # @return [Polygon] A new {Polygon} with orientation that's the opposite of the receiver def reverse self.class.new *(self.vertices.reverse) end # Reverse the receiver and return it # @return [Polygon] the reversed receiver def reverse! super # Simply reversing the vertex array causes the reversed polygon to # start at what had been the last vertex, instead of starting at # the same vertex and just going the other direction. vertices.unshift vertices.pop self end # @group Boolean operators # Test a {Point} for inclusion in the receiver using a simplified winding number algorithm # @param [Point] point The {Point} to test # @return [Number] 1 if the {Point} is inside the {Polygon}, -1 if it's outside, and 0 if it's on an {Edge} def <=>(point) sum = edges.reduce(0) do |sum, e| direction = e.last.y <=> e.first.y # Ignore edges that don't cross the point's x coordinate next sum unless ((point.y <=> e.last.y) + (point.y <=> e.first.y)).abs <= 1 if 0 == direction # Special case horizontal edges return 0 if ((point.x <=> e.last.x) + (point.x <=> e.first.x)).abs <= 1 next sum # Doesn't intersect else is_left = e <=> point return 0 if 0 == is_left next sum unless is_left sum += 0 <=> (direction + is_left) end end (0 == sum) ? -1 : 1 end # Create a new {Polygon} that's the union of the receiver and a passed {Polygon} # This is a simplified implementation of the alogrithm outlined in the # paper {http://gvu.gatech.edu/people/official/jarek/graphics/papers/04PolygonBooleansMargalit.pdf An algorithm for computing the union, intersection or difference of two polygons}. # 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. # @param [Polygon] other The {Polygon} to union with the receiver # @return [Polygon] The union of the receiver and the passed {Polygon} def union(other) # Table 1: Both polygons are islands and the operation is union, so both must have the same orientation # Reverse the other polygon if the orientations are different other = other.reverse if self.clockwise? != other.clockwise? # Receiver's vertex ring ringA = VertexRing.new self.vertices.each {|v| ringA.push v, (other <=> v)} # The other vertex ring ringB = VertexRing.new other.vertices.each {|v| ringB.push v, (self <=> v)} # Find intersections offsetA = 0 edgesB = other.edges.dup self.edges.each_with_index do |a, indexA| offsetB = 0 ringB.edges_with_index do |b, indexB| intersection = a.intersection(b) if intersection === true if (a.first == b.first) and (a.last == b.last) # Equal edges elsif (a.first == b.last) and (a.last == b.first) # Ignore equal but opposite edges else if a.vector.normalize == b.vector.normalize # Same direction? offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, b.first) offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, a.last) else # Opposite direction offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, b.last) offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, a.first) end end elsif intersection.is_a?(Point) offsetA += 1 if ringA.insert_boundary(indexA + 1 + offsetA, intersection) offsetB += 1 if ringB.insert_boundary(indexB + 1 + offsetB, intersection) end end end # Table 2: Both polygons are islands and the operation is union, so select outside from both polygons edgeFragments = [] [[ringA, other], [ringB, self]].each do |ring, other_polygon| ring.edges do |v1,v2| if (v1[:type] == -1) or (v2[:type] == -1) edgeFragments.push :first => v1[:vertex], :last => v2[:vertex] elsif (v1[:type] == 0) and (v2[:type] == 0) if (other_polygon <=> Point[(v1[:vertex] + v2[:vertex])/2]) <= 0 edgeFragments.push :first => v1[:vertex], :last => v2[:vertex] end end end end # Delete any duplicated edges. Array#uniq doesn't do the right thing, so using inject instead. edgeFragments = edgeFragments.inject([]) {|result,h| result << h unless result.include?(h); result} # Delete any equal-and-opposite edges edgeFragments = edgeFragments.reject {|f| edgeFragments.find {|f2| (f[:first] == f2[:last]) and (f[:last] == f2[:first])} } # Construct the output polygons output = edgeFragments.reduce([Array.new]) do |output, fragment| next output if fragment.empty? polygon = output.last polygon.push fragment[:first], fragment[:last] if polygon.empty? while 1 do adjacent_fragment = edgeFragments.find {|f| fragment[:last] == f[:first]} break unless adjacent_fragment polygon.push adjacent_fragment[:first], adjacent_fragment[:last] fragment = adjacent_fragment.dup adjacent_fragment.clear break if polygon.first == polygon.last # closed? end output << Array.new end # If everything worked properly there should be only one output Polygon output.reject! {|a| a.empty?} output = Polygon.new *(output[0]) # Table 4: Both input polygons are "island" type and the operation # is union, so the output polygon's orientation should be the same # as the input polygon's orientation (self.clockwise? != output.clockwise?) ? output.reverse : output end alias :+ :union # @endgroup # @group Convex Hull # Returns the convex hull of the {Polygon} # @return [Polygon] A convex {Polygon}, or the original {Polygon} if it's already convex def convex wrap end # Returns the convex hull using the {http://en.wikipedia.org/wiki/Gift_wrapping_algorithm Gift Wrapping algorithm} # This implementation was cobbled together from many sources, but mostly from this implementation of the {http://butunclebob.com/ArticleS.UncleBob.ConvexHullTiming Jarvis March} # @return [Polygon] def wrap # Start with a Point that's guaranteed to be on the hull leftmost_point = vertices.min_by {|v| v.x} current_point = vertices.select {|v| v.x == leftmost_point.x}.min_by {|v| v.y} current_angle = 0.0 hull_points = [current_point] while true min_angle = 4.0 min_point = nil vertices.each do |v1| next if current_point.equal? v1 angle = pseudo_angle_for_edge(current_point, v1) min_point, min_angle = v1, angle if (angle >= current_angle) && (angle <= min_angle) end current_angle = min_angle current_point = min_point break if current_point == hull_points.first hull_points << min_point end Polygon.new *hull_points end # @endgroup # Outset the receiver by the specified distance # @param [Number] distance The distance to offset by # @return [Polygon] A new {Polygon} outset by the given distance def outset(distance) bisector_edges = outset_bisectors(distance) bisector_pairs = bisector_edges.push(bisector_edges.first).each_cons(2) # 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 active_edges.each_with_index do |e, i| e1 = e[:edge] next unless e1 # Ignore deleted edges intersection, j = find_last_intersection(active_edges, i, e1) if intersection e2 = active_edges[j][:edge] wrap_around_is_shortest = ((i + active_edges.count - j) < (j-i)) if intersection.is_a? Point if wrap_around_is_shortest active_edges[i][:edge] = Edge.new(intersection, e1.last) active_edges[j][:edge] = Edge.new(e2.first, intersection) else active_edges[i][:edge] = Edge.new(e1.first, intersection) active_edges[j][:edge] = Edge.new(intersection, e2.last) end else # Handle the collinear case active_edges[i][:edge] = Edge.new(e1.first, e2.last) active_edges[j].delete(:edge) wrap_around_is_shortest = false end # Delete everything between e1 and e2 if wrap_around_is_shortest # Choose the shortest path for k in 0...i do active_edges[k].delete(:edge) end for k in j...active_edges.count do next if k==j # Exclude e2 active_edges[k].delete(:edge) end else for k in i...j do next if k==i # Exclude e1 and e2 active_edges[k].delete(:edge) end end redo # Recheck the modified edges end end Polygon.new *(active_edges.map {|e| e[:edge]}.compact.map {|e| [e.first, e.last]}.flatten) end # Vertex bisectors suitable for outsetting # @param [Number] length The distance to offset by # @return [Array] {Edge}s representing the bisectors def outset_bisectors(length) vertices.zip(spokes).map {|v,b| b ? Edge.new(v, v+(b * length)) : nil} end # Generate the unit-length spokes for each vertex # @return [Array] the unit {Vector}s representing the spoke of each vertex def spokes clockwise? ? left_bisectors : right_bisectors end private # Return a number that increases with the slope of the {Edge} # @return [Number] A number in the range [0,4) def pseudo_angle_for_edge(point0, point1) delta = Point[point1.x.to_f, point1.y.to_f] - Point[point0.x.to_f, point0.y.to_f] if delta.x >= 0 if delta.y >= 0 quadrant_one_psuedo_angle(delta.x, delta.y) else 1 + quadrant_one_psuedo_angle(delta.y.abs, delta.x) end else if delta.y >= 0 3 + quadrant_one_psuedo_angle(delta.y, delta.x.abs) else 2 + quadrant_one_psuedo_angle(delta.x.abs, delta.y.abs) end end end def quadrant_one_psuedo_angle(dx, dy) dx / (dx + dy) end end private class VertexRing attr_reader :vertices def initialize @vertices = [] end # @param [Integer] index The index to insert the new {Point} before # @param [Point] point The {Point} to insert # @param [Integer] type The vertex type: 1 is inside, 0 is boundary, -1 is outside def insert(index, point, type) if v = @vertices.find {|v| v[:vertex] == point } v[:type] = type false else @vertices.insert(index, {:vertex => point, :type => type}) true end end # Insert a boundary vertex # @param [Integer] index The index to insert the new {Point} before # @param [Point] point The {Point} to insert def insert_boundary(index, point) self.insert(index, point, 0) end # @param [Point] point The {Point} to push # @param [Integer] type The vertex type: 1 is inside, 0 is boundary, -1 is outside def push(point, type) @vertices << {:vertex => point, :type => type} end # Enumerate the pairs of vertices corresponding to each edge def edges (@vertices + [@vertices.first]).each_cons(2) {|v1,v2| yield v1, v2} end def edges_with_index index = 0 (@vertices + [@vertices.first]).each_cons(2) {|v1,v2| yield(Edge.new(v1[:vertex], v2[:vertex]), index); index += 1} end end end