Class: HeatingSystemCalculator::MatFitter
- Inherits:
-
Object
- Object
- HeatingSystemCalculator::MatFitter
- Defined in:
- app/services/heating_system_calculator/mat_fitter.rb
Overview
This is a 2D fit-first greedy heuristic algorithm that tries to fit each box into the trunk, largest first. If no box fits,
a new trunk is created.
I'm representing trunks as 2D tables of squares (with one unit of hidden padding around), then fill it starting
from top left. The fitter-first finds a row with enough empty space to fit the box and then checks if the next box-height
rows also contain the space. If they do, the box is drawn to the rows.
Printing is easy because the trunk already is in printable format.
Not a particularly fast way to do it, mind you :)
If you're interested about different algorithms for solving the 2D bin packing problem,
here's a paper:http://www.dei.unipd.it/~fisch/ricop/tesi/tesi_dottorato_Lodi_1999.pdf
Instance Attribute Summary collapse
-
#coordinates ⇒ Object
readonly
Returns the value of attribute coordinates.
-
#items ⇒ Object
readonly
Returns the value of attribute items.
Delegated Instance Attributes collapse
-
#empty? ⇒ Object
Alias for @items#empty?.
Instance Method Summary collapse
-
#add(mat) ⇒ Object
try adding the mat both ways, if either fits we assume its now in the Zone and it will be deleted from the list of mats.
- #fit_mats(mats) ⇒ Object
-
#initialize(w, h) ⇒ MatFitter
constructor
A new instance of MatFitter.
- #rotate(mat) ⇒ Object
-
#to_s ⇒ Object
this formatting method is called before the zone is printed.
- #try_adding(mat, rotated = false) ⇒ Object
Constructor Details
#initialize(w, h) ⇒ MatFitter
Returns a new instance of MatFitter.
21 22 23 24 25 26 27 |
# File 'app/services/heating_system_calculator/mat_fitter.rb', line 21 def initialize(w, h) @w = w @h = h @items = [] @coordinates = [] @rows = (1..@h).map { '_' * @w } end |
Instance Attribute Details
#coordinates ⇒ Object (readonly)
Returns the value of attribute coordinates.
19 20 21 |
# File 'app/services/heating_system_calculator/mat_fitter.rb', line 19 def coordinates @coordinates end |
#items ⇒ Object (readonly)
Returns the value of attribute items.
19 20 21 |
# File 'app/services/heating_system_calculator/mat_fitter.rb', line 19 def items @items end |
Instance Method Details
#add(mat) ⇒ Object
try adding the mat both ways, if either fits we assume its now
in the Zone and it will be deleted from the list of mats
31 32 33 |
# File 'app/services/heating_system_calculator/mat_fitter.rb', line 31 def add(mat) try_adding(mat) or try_adding(rotate(mat), true) end |
#empty? ⇒ Object
Alias for @items#empty?
83 |
# File 'app/services/heating_system_calculator/mat_fitter.rb', line 83 delegate :empty?, to: :@items |
#fit_mats(mats) ⇒ Object
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'app/services/heating_system_calculator/mat_fitter.rb', line 91 def fit_mats(mats) # sort the mats from largest to smallest based on their area (w*h) mats = mats.sort_by { |b| b[0] * b[1] }.reverse # while there are still mats to try this loop will try to fit them # into the zone. We are going through the mats from largest to smallest # If a big mat doesn't have room there, all the smaller mats will be tried done_filling_zone = false until done_filling_zone # try to add the mat to the last zone, if it doesn't go we get nil fitting = mats.find { |mat| add mat } # if the mat fit if fitting # remove the mat from the mats that need to be placed # mats.delete_first fitting # if the zone is empty elsif empty? # raise an error because the mat is too big to fit into the zone # and a solution is impossible # puts "Can't fit #{mats.inspect} into the zone" return { items:, coordinates: } # mat didn't fit but the zone has mats in it else done_filling_zone = true end end { items:, coordinates: } end |
#rotate(mat) ⇒ Object
78 79 80 |
# File 'app/services/heating_system_calculator/mat_fitter.rb', line 78 def rotate(mat) [mat[1], mat[0], mat[2]] end |
#to_s ⇒ Object
this formatting method is called before the zone is printed.
the extra space is stripped out of the zone here
87 88 89 |
# File 'app/services/heating_system_calculator/mat_fitter.rb', line 87 def to_s @rows[0..@h].pluck(0..@w).join("\n") end |
#try_adding(mat, rotated = false) ⇒ Object
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 |
# File 'app/services/heating_system_calculator/mat_fitter.rb', line 35 def try_adding(mat, rotated = false) # puts "try adding mat: #{mat.inspect}, index #{mat[2]}" # create a new mat row. Also note that matrow # is built as a String of "_" characters, so it will be used to find empty Zone space. matrow = '_' * mat[0] @rows.each_with_index do |r, i| # break out of the loop as soon as we run out of enough @rows to hold the mat (and 2 extra padding @rows) break if i > @rows.size - mat[1] # verify that this row holds the mat, or move on next unless r.include?(matrow) # fetch the index where the mat would fit on @rows needed to hold it below this one idxs = @rows[i + 1, mat[1] + 1].map { |s| s.index matrow } # verify that we got an index for all needed lines, or move on. next unless idxs.all? # find the highest of those indices. idx = idxs.max # verify that the mat does fit on all needed @rows at the highest found index, or move on next unless @rows[i, mat[1]].all? { |s| s[idx, matrow.size] == matrow } # if we made it this far, we have a match, so we will draw the mat in place. We can # ignore the padding now, since we already proved there is room. @rows[i, mat[1]].each { |s| s[idx, mat[0]] = mat[2].first.to_s * mat[0] } # add the mat to the @items, so we know the Zone isn't empty?(), and return the mat to indicate # a match @items.push mat # store coordinate for rendering @coordinates.push [idx, i, rotated] # puts "mat: #{mat.inspect} did fit" return mat end # puts "mat: #{mat.inspect} did not fit" nil # default false return, if we didn't find a match end |