shawn42/gamebox

View on GitHub
lib/gamebox/lib/rect.rb

Summary

Maintainability
D
2 days
Test Coverage
#--
#  Rubygame -- Ruby code and bindings to SDL to facilitate game creation
#  Copyright (C) 2004-2007  John Croisant
#
#  This library is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2.1 of the License, or (at your option) any later version.
#
#  This library is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#  Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with this library; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#++

#--
# Table of Contents:
#
# class Rect
#   GENERAL:
#     initialize
#     new_from_object
#     to_s
#     to_a, to_ary
#     []
#   ATTRIBUTES:
#     x, y, w, h [<- accessors]
#     width, height, size
#     left, top, right, bottom
#     center, centerx, centery
#     topleft, topright
#     bottomleft, bottomright
#     midleft, midtop, midright, midbottom
#   UTILITY METHODS:
#     clamp, clamp!
#     clip, clip!
#     collide_hash, collide_hash_all
#     collide_array, collide_array_all
#     collide_point?
#     collide_rect?
#     contain?
#     inflate, inflate!
#     move, move!
#     normalize, normalize!
#     union, union!
#     union_all, union_all!
#
#  class Surface
#    make_rect
# 
#++

#module Rubygame

# A Rect is a representation of a rectangle, with four core attributes
# (x offset, y offset, width, and height) and a variety of functions
# for manipulating and accessing these attributes.
#
# Like all coordinates in Rubygame (and its base library, SDL), x and y
# offsets are measured from the top-left corner of the screen, with greater
# y offsets being lower. Thus, specifying the x and y offsets of the Rect
# is equivalent to setting the location of its top-left corner.
# 
# In Rubygame, Rects are used for collision detection and describing 
# the area of a Surface to operate on.
class Rect < Array
  include MinMaxHelpers

  #--
  # GENERAL
  #++

  # Create a new Rect, attempting to extract its own information from 
  # the given arguments. The arguments must fall into one of these cases:
  # 
  #   - 4 integers +(x, y, w, h)+.
  #   - 1 Rect or Array containing 4 integers +([x, y, w, h])+.
  #   - 2 Arrays containing 2 integers each +([x,y], [w,h])+.
  #   - 1 object with a +rect+ attribute which is a valid Rect object.
  # 
  # All rect core attributes (x,y,w,h) must be integers.
  # 
  def initialize(*argv)
    case argv.length
    when 1
      if argv[0].kind_of? Array; super(argv[0])
      elsif argv[0].respond_to? :rect; super(argv[0])
      end
    when 2
      super(argv[0].concat(argv[1]))
    when 4
      super(argv)
    end
    return self
  end

  # Extract or generate a Rect from the given object, if possible, using the
  # following process:
  # 
  #  1. If it's a Rect already, return a duplicate Rect.
  #  2. Elsif it's an Array with at least 4 values, make a Rect from it.
  #  3. Elsif it has a +rect+ attribute., perform (1) and (2) on that.
  #  4. Otherwise, raise TypeError.
  # 
  # See also Surface#make_rect()
  def Rect.new_from_object(object)
    case(object)
    when Rect
      return object.dup
    when Array 
      if object.length >= 4
        return Rect.new(object)
      else
        raise(ArgumentError,"Array does not have enough indices to be made into a Rect (%d for 4)."%object.length )
      end
    else
      begin
        case(object.rect)
        when Rect
          return object.rect.dup
        when Array
          if object.rect.length >= 4
            return Rect.new(object.rect)
          else
            raise(ArgumentError,"Array does not have enough indices to be made into a Rect (%d for 4)."%object.rect.length )
          end
        end        # case object.rect
      rescue NoMethodError # if no rect.rect
        raise(TypeError,"Object must be a Rect or Array [x,y,w,h], or have an attribute called 'rect'. (Got %s instance.)"%object.class)
      end
    end # case object
  end


  # Print the Rect in the form "+#<Rect [x,y,w,h]>+"
  def to_s; "#<Rect [%s,%s,%s,%s]>"%self; end

  # Print the Rect in the form "+#<Rect:id [x,y,w,h]>+"
  def inspect; "#<Rect:#{self.object_id} [%s,%s,%s,%s]>"%self; end


  #--
  # ATTRIBUTES
  #++

  # Returns self[0]
  def x; return self[0]; end
  # Sets self[0] to +val+
  def x=(val); self[0] = val; end

  alias left x
  alias left= x=;
  alias l x
  alias l= x=;

  # Returns self[1]
  def y; return self[1]; end
  # Sets self[1] to +val+
  def y=(val); self[1] = val; end

  alias top y
  alias top= y=;
  alias t y
  alias t= y=;

  # Returns self[2]
  def w; return self[2]; end
  # Sets self[2] to +val+
  def w=(val); self[2] = val; end

  alias width w
  alias width= w=;

  # Returns self[3]
  def h; return self[3]; end
  # Sets self[3] to +val+
  def h=(val); self[3] = val; end

  alias height h
  alias height= h=;

  # Return the width and height of the Rect.
  def size; return self[2,2]; end

  # Set the width and height of the Rect.
  def size=(size)
   raise ArgumentError, "Rect#size= takes an Array of form [width, height]." if size.size != 2
   self[2,2] = size
   size
  end

  # Return the x coordinate of the right side of the Rect.
  def right; return self[0]+self[2]; end

  # Set the x coordinate of the right side of the Rect by translating the
  # Rect (adjusting the x offset).
  def right=(r); self[0] = r - self[2]; return r; end

  alias r right
  alias r= right=;

  # Return the y coordinate of the bottom side of the Rect.
  def bottom; return self[1]+self[3]; end

  # Set the y coordinate of the bottom side of the Rect by translating the
  # Rect (adjusting the y offset).
  def bottom=(b); self[1] = b - self[3]; return b; end

  alias b bottom
  alias b= bottom=;

  # Return the x and y coordinates of the center of the Rect.
  def center; return self.centerx, self.centery; end

  # Set the x and y coordinates of the center of the Rect by translating the
  # Rect (adjusting the x and y offsets).
  def center=(center)
      raise ArgumentError, "Rect#center= takes an Array of the form [x,y]." if center.size != 2
    self.centerx, self.centery = center
    center
  end
  alias c center
  alias c= center=;

  # Return the x coordinate of the center of the Rect
  def centerx; return self[0]+(self[2].div(2)); end

  # Set the x coordinate of the center of the Rect by translating the
  # Rect (adjusting the x offset).
  def centerx=(x); self[0] = x - (self[2].div(2)); return x; end

  alias cx centerx
  alias cx= centerx=;

  # Return the y coordinate of the center of the Rect
  def centery; return self[1]+(self[3].div(2)); end

  # Set the y coordinate of the center of the Rect by translating the
  # Rect (adjusting the y offset).
  def centery=(y); self[1] = y- (self[3].div(2)); return y; end

  alias cy centery
  alias cy= centery=;

  # Return the x and y coordinates of the top-left corner of the Rect
  def topleft; return self[0,2].to_a; end

  # Set the x and y coordinates of the top-left corner of the Rect by 
  # translating the Rect (adjusting the x and y offsets).
  def topleft=(topleft)
      raise ArgumentError, "Rect#topright= takes an Array of form [x, y]." if topleft.size != 2
    self[0,2] = topleft
    return topleft
  end

  alias tl topleft
  alias tl= topleft=;

  # Return the x and y coordinates of the top-right corner of the Rect
  def topright; return self.right, self[1]; end

  # Set the x and y coordinates of the top-right corner of the Rect by 
  # translating the Rect (adjusting the x and y offsets).
  def topright=(topright)
      raise ArgumentError, "Rect#topright= takes an Array of form [x, y]." if topright.size != 2
    self.right, self[1] = topright
    return topright
  end

  alias tr topright
  alias tr= topright=;

  # Return the x and y coordinates of the bottom-left corner of the Rect
  def bottomleft; return self[0], self.bottom; end

  # Set the x and y coordinates of the bottom-left corner of the Rect by 
  # translating the Rect (adjusting the x and y offsets).
  def bottomleft=(bottomleft)
    raise ArgumentError, "Rect#bottomleft= takes an Array of form [x, y]." if bottomleft.size != 2
    self[0], self.bottom = bottomleft
    return bottomleft
  end

  alias bl bottomleft
  alias bl= bottomleft=;

  # Return the x and y coordinates of the bottom-right corner of the Rect
  def bottomright; return self.right, self.bottom; end

  # Set the x and y coordinates of the bottom-right corner of the Rect by 
  # translating the Rect (adjusting the x and y offsets).
  def bottomright=(bottomright)
    raise ArgumentError, "Rect#bottomright= takes an Array of form [x, y]." if bottomright.size != 2
    self.right, self.bottom = bottomright
    return bottomright
  end

  alias br bottomright
  alias br= bottomright=;

  # Return the x and y coordinates of the midpoint on the left side of the
  # Rect.
  def midleft; return self[0], self.centery; end  

  # Set the x and y coordinates of the midpoint on the left side of the Rect
  # by translating the Rect (adjusting the x and y offsets).
  def midleft=(midleft)
      raise ArgumentError, "Rect#midleft= takes an Array of form [x, y]." if midleft.size != 2
    self[0], self.centery = midleft
    return midleft
  end

  alias ml midleft
  alias ml= midleft=;

  # Return the x and y coordinates of the midpoint on the left side of the
  # Rect.
  def midtop; return self.centerx, self[1]; end  

  # Set the x and y coordinates of the midpoint on the top side of the Rect
  # by translating the Rect (adjusting the x and y offsets).
  def midtop=(midtop)
      raise ArgumentError, "Rect#midtop= takes an Array of form [x, y]." if midtop.size != 2
    self.centerx, self[1] = midtop
    return midtop
  end

  alias mt midtop
  alias mt= midtop=;

  # Return the x and y coordinates of the midpoint on the left side of the
  # Rect.
  def midright; return self.right, self.centery; end  

  # Set the x and y coordinates of the midpoint on the right side of the Rect
  # by translating the Rect (adjusting the x and y offsets).
  def midright=(midright)
      raise ArgumentError, "Rect#midright= takes an Array of form [x, y]." if midright.size != 2
    self.right, self.centery = midright
    return midright
  end

  alias mr midright
  alias mr= midright=;

  # Return the x and y coordinates of the midpoint on the left side of the
  # Rect.
  def midbottom; return self.centerx, self.bottom; end  

  # Set the x and y coordinates of the midpoint on the bottom side of the
  # Rect by translating the Rect (adjusting the x and y offsets).
  def midbottom=(midbottom)
      raise ArgumentError, "Rect#midbottom= takes an Array of form [x, y]." if midbottom.size != 2
    self.centerx, self.bottom = midbottom
    return midbottom
  end

  alias mb midbottom
  alias mb= midbottom=;

  #--
  # UTILITY METHODS
  #++


  # As #clamp!, but the original caller is not changed.
  def clamp(rect)
    self.dup.clamp!(rect)
  end

  # Translate the calling Rect to be entirely inside the given Rect. If 
  # the caller is too large along either axis to fit in the given rect,
  # it is centered with respect to the given rect, along that axis.
  def clamp!(rect)
    nself = self.normalize
    rect = Rect.new_from_object(rect)
    #If self is inside given, there is no need to move self
    unless rect.contain?(nself)

      #If self is too wide:
      if nself[2] >= rect[2]
        self[0] = rect.centerx - nself[2].div(2)
        #Else self is not too wide
      else
        #If self is to the left of arg
        if nself[0] < rect[0]
          self[0] = rect[0]
        #If self is to the right of arg
        elsif nself.right > rect.right
          self[0] = rect.right - nself[2]
        #Otherwise, leave x alone
        end
      end

      #If self is too tall:
      if nself[3] >= rect[3]
        self[1] = rect.centery - nself[3].div(2)
        #Else self is not too tall
      else
        #If self is above arg
        if nself[1] < rect[1]
          self[1] = rect[1]
        #If self below arg
        elsif nself.bottom > rect.bottom
          self[1] = rect.bottom - nself[3]
        #Otherwise, leave y alone
        end
      end
    end
    return self
  end

  # As #clip!, but the original caller is not changed.
  def clip(rect)
    self.dup.clip!(rect)
  end

  # Crop the calling Rect to be entirely inside the given Rect. If the
  # caller does not intersect the given Rect at all, its width and height
  # are set to zero, but its x and y offsets are not changed.
  # 
  # As a side effect, the Rect is normalized.
  def clip!(rect)
    nself = self.normalize
    other = Rect.new_from_object(rect).normalize!
    if self.collide_rect?(other)
      self[0] = [nself[0], other[0]].max
      self[1] = [nself[1], other[1]].max
      self[2] = [nself.right, other.right].min - self[0]
      self[3] = [nself.bottom, other.bottom].min - self[1]
    else #if they do not intersect at all:
      self[0], self[1] = nself.topleft
      self[2], self[3] = 0, 0
    end
    return self
  end

  # Iterate through all key/value pairs in the given hash table, and
  # return the first pair whose value is a Rect that collides with the
  # caller.
  #
  # Because a hash table is unordered, you should not expect any 
  # particular Rect to be returned first.
  def collide_hash(hash_rects)
    hash_rects.each { |key,value|
      if value.collide_rect?+(self); return [key,value]; end
    }
    return nil
  end

  # Iterate through all key/value pairs in the given hash table, and
  # return an Array of every pair whose value is a Rect that collides
  # the caller.
  # 
  # Because a hash table is unordered, you should not expect the returned
  # pairs to be in any particular order.
  def collide_hash_all(hash_rects)
    hash_rects.select { |key,value|
      value.collide_rect?+(self)
    }
  end

  # Iterate through all elements in the given Array, and return
  # the *index* of the first element which is a Rect that collides with
  # the caller.
  def collide_array(array_rects)
    for i in (0...(array_rects.length))
      if array_rects[i].collide_rect?(self)
        return i
      end
    end
    return nil
  end

  # Iterate through all elements in the given Array, and return
  # an Array containing the *indices* of every element that is a Rect
  # that collides with the caller.
  def collide_array_all(array_rects)
    indexes = []
    for i in (0...(array_rects.length))
      if array_rects[i].collide_rect?(self)
        indexes += [i]
      end
    end
    return indexes
  end

  # True if the point is inside (including on the border) of the caller.
  # If you have Array of coordinates, you can use collide_point?(*coords).
  def collide_point?(x,y)
    nself = normalize()
    x.between?(nself.left,nself.right) && y.between?(nself.top,nself.bottom)
  end

  # True if the caller and the given Rect overlap (or touch) at all.
  def collide_rect?(rect)
    rect = Rect.new_from_object(rect)
    ((l >= rect.l && l <= rect.r) or (rect.l >= l && rect.l <= r)) &&
    ((t >= rect.t && t <= rect.b) or (rect.t >= t && rect.t <= b))
  end

  # True if the given Rect is totally within the caller. Borders may
  # overlap.
  def contain?(rect)
    nself = self#.normalize
    rect = Rect.new_from_object(rect)#.normalize!
    return (nself.left <= rect.left and rect.right <= nself.right and
          nself.top <= rect.top and rect.bottom <= nself.bottom)
  end

  # As #inflate!, but the original caller is not changed.
  def inflate(x,y)
    return self.class.new(self[0] - x.div(2),
                          self[1] - y.div(2),
                          self[2] + x,
                          self[3] + y)
  end

  # Increase the Rect's size is the x and y directions, while keeping the
  # same center point. For best results, expand by an even number.
  # X and y inflation can be given as an Array or as separate values.
  def inflate!(x,y)
    self[0] -= x.div(2)
    self[1] -= y.div(2)
    self[2] += x
    self[3] += y
    return self
  end

  # As #move!, but the original caller is not changed.
  def move(x,y)
    self.dup.move!(x,y)
  end

  # Translate the Rect by the given amounts in the x and y directions.
  # Positive values are rightward for x and downward for y.
  # X and y movement can be given as an Array or as separate values.
  def move!(x,y)
    self[0]+=x; self[1]+=y
    return self
  end

  # As #normalize!, but the original caller is not changed.
  def normalize
    self.dup.normalize!()
  end

  # Fix Rects that have negative width or height, without changing the
  # area it represents. Has no effect on Rects with non-negative width
  # and height. Some Rect methods will automatically normalize the Rect.
  def normalize!
    if self[2] < 0
      self[0], self[2] = self[0]+self[2], -self[2]
    end
    if self[3] < 0
      self[1], self[3] = self[1]+self[3], -self[3]
    end
    self
  end

  # As #union!, but the original caller is not changed.
  def union(rect)
    self.dup.union!(rect)
  end

  # Expand the caller to also cover the given Rect. The Rect is still a 
  # rectangle, so it may also cover areas that neither of the original
  # Rects did, for example areas between the two Rects.
  def union!(rect)
    self.normalize!
    rleft, rtop = self.topleft
    rright, rbottom = self.bottomright
    r2 = Rect.new_from_object(rect).normalize!

    rleft = [rleft, r2.left].min
    rtop = [rtop, r2.top].min
    rright = [rright, r2.right].max
    rbottom = [rbottom, r2.bottom].max

    self[0,4] = rleft, rtop, rright - rleft, rbottom - rtop
    return self
  end

  # As #union_all!, but the original caller is not changed.
  def union_all(array_rects)
    self.dup.union_all!(array_rects)
  end

  # Expand the caller to cover all of the given Rects. See also #union!
  def union_all!(array_rects)
    array_rects.each do |r|
      self.union!(r)
    end
    return self
  end

  # calculate the area of the Rect
  def area
    w * h
  end

  def zero_out!
    self[0] = 0
    self[1] = 0
    self[2] = 0
    self[3] = 0
  end

  def union_area(rect)
    rleft = self.left
    rtop = self.top
    rright = self.right
    rbottom = self.bottom
    r2 = Rect.new_from_object(rect)

    rleft = min(rleft, r2.left)
    rtop = min(rtop, r2.top)
    rright = max(rright, r2.right)
    rbottom = max(rbottom, r2.bottom)

    (rright - rleft) * (rbottom - rtop)
  end

  def union_fast(rect)
    rleft = self.left
    rtop = self.top
    rright = self.right
    rbottom = self.bottom
    r2 = Rect.new_from_object(rect)

    rleft = min(rleft, r2.left)
    rtop = min(rtop, r2.top)
    rright = max(rright, r2.right)
    rbottom = max(rbottom, r2.bottom)

    Rect.new(rleft, rtop, rright - rleft, rbottom - rtop)
  end

  def expand_to_include!(rect)
    rleft = self.left
    rtop = self.top
    rright = self.right
    rbottom = self.bottom
    r2 = Rect.new_from_object(rect)

    rleft = min(rleft, r2.left)
    rtop = min(rtop, r2.top)
    rright = max(rright, r2.right)
    rbottom = max(rbottom, r2.bottom)

    self.x = rleft
    self.y = rtop
    self.w = rright - rleft
    self.h = rbottom - rtop
  end

  def refit_for!(rect_a, rect_b)
    self.zero_out!
    self.expand_to_include! rect_a
    self.expand_to_include! rect_b
  end

end # class Rect

#end # module Rubygame