lib/gamebox/lib/rect.rb
#--
# 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