```My solution so far.
I use 'BoxSorter' to keep all the boxes in a hash indexed by both
dimensions so its easy to find one that fits.  The 'TrunkSet'  keeps
track of the loaded boxes using arrays of characters.
The algorithm is kind of the opposite of Ilmari's: it iterates over
empty spaces in the trunk, looking for boxes that fit, instead of
iterating over the boxes.

-----------------------------------------------------------------

require 'delegate'
class Box
def initialize w,h
@w,@h=w,h
end
def [] n
n==0 ? @w :@h
end
def rotate!
@w,@h = @h,@w
end
def other n
n==@h ? @w :@h
end
def <=> box2   #compares the non-matching dimension
if @w == box2[0] then @h <=> box2[1]
elsif @w== box2[1] then @h <=> box2[0]
elsif @h == box2[0] then @w <=> box2[1]
else @w <=> box2[0]
end
end
end

class BoxSorter
def initialize boxset
@h = Hash.new
boxset.each{|b|@h[b[0]]||=[];@h[b[0]]<<b;@h[b[1]]||=[];@h[b[1]]<<b; @h}
@h.each {|k,v| (v.sort!||v).reverse!}
# hash has key for each box side,
#  containing array of boxes sorted by size of the other side
end
def size
@h.size
end
def find_best_fit w,h
while w>0
set = @h[w]
box = set.find{|b| b.other(w)<=h } if set
if box
self.remove box
box.rotate! if box[0] != w
return box
end
w-=1;
end
end
def remove box
@h.delete_if {|k,v| v.delete(box); v.empty? }
end
end

class TrunkSet < DelegateClass(Array)
def initialize w,h
@width,@height=w,h
super []
grow
end
def grow
@openrow=0
self<< Array.new(@height){Array.new(@width){" "}}
end

def first_open_row
loop do
break if last[@openrow].find{|s| s==' '}
grow if @height == (@openrow +=1)
end
last[@openrow]
end
def first_open_space
gaps,lastchar = [],nil
first_open_row.each_with_index do |c,i|
if c==' '
if c==lastchar then gaps[-1][0]+=1
else                gaps << [1,i]; end
end
lastchar = c
end
gaps.max
end
last[@openrow].map!{|c| if c==' ' then '+' else c end }
first_open_row
end

size,height = box[0],box[1]
(0..height).each do |row|
fillchar = (row == height) ? ['+','-'] : ['|','#']
if nil != (fillrow = last[@openrow+row])
fillrow[col-1] = fillchar[0] if (col-1>=0 )
size.times {|i| fillrow[col+i] = fillchar[1] }
fillrow[col+size] = fillchar[0] if ( col+size < @width )
end
end
end

def rows_remaining
@height-@openrow
end
def has_no_boxes?
last.each{|r| return false if r.find{|c| c == '#'} }
true
end
end

class Packer
def initialize size, boxes
@loose_boxes = BoxSorter.new(boxes)
@trunks = TrunkSet.new(*size)
end

def pack_a_box
column,nextbox = nil,nil
loop do
space_available,column = @trunks.first_open_space
nextbox = @loose_boxes.find_best_fit(space_available,
@trunks.rows_remaining)
break if nextbox
end
end

def pack
until @loose_boxes.size == 0
pack_a_box
end
end

def show
@trunks.pop if @trunks.has_no_boxes?
@trunks.each do |bin|
bin.each { |row|puts row.join }
puts ""
end
end
end

class PackerParser
def initialize file
end
def size
@binsize.match(/(\d*)x(\d*)/)
[\$1.to_i,\$2.to_i]
end
def boxes
@blocks.map{|s| s.match(/(\d*)x(\d*)/);Box.new(\$1.to_i,\$2.to_i)}
end
end

if __FILE__ == \$0
pp = PackerParser.new(ARGF)
puts pp.binsize
puts pp.blocks.join(' ')

pk = Packer.new(pp.size, pp.boxes)
pk.pack
pk.show
end

```