From cc26b98909cf4eeb02fb993c657de5824698d864 Mon Sep 17 00:00:00 2001 From: John Bintz Date: Mon, 16 May 2011 11:30:14 -0400 Subject: [PATCH] start work on better packing algorithm, start with scan --- Gemfile.lock | 4 +- .../sass_extensions/sprites/image_group.rb | 18 ++++ .../sass_extensions/sprites/image_row.rb | 27 ++++-- .../sass_extensions/sprites/row_fitter.rb | 81 ++++++++++++++++++ test.watchr | 7 ++ test/test_helper.rb | 4 +- test/units/sprites/image_group_test.rb | 9 ++ test/units/sprites/image_row_test.rb | 6 +- test/units/sprites/row_fitter_test.rb | 82 +++++++++++++++++++ 9 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 lib/compass/sass_extensions/sprites/image_group.rb create mode 100644 lib/compass/sass_extensions/sprites/row_fitter.rb create mode 100644 test.watchr create mode 100644 test/units/sprites/image_group_test.rb create mode 100644 test/units/sprites/row_fitter_test.rb diff --git a/Gemfile.lock b/Gemfile.lock index 90081653..152232a4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,7 +7,7 @@ GIT PATH remote: . specs: - compass (0.11.1.d9e4054) + compass (0.11.1.d9ca08f) chunky_png (~> 1.1) fssm (>= 0.2.7) sass (~> 3.1) @@ -50,7 +50,7 @@ GEM autotest-fsevent (0.2.5) sys-uname builder (2.1.2) - chunky_png (1.1.1) + chunky_png (1.2.0) compass-validator (3.0.0) css_parser (1.0.1) cucumber (0.9.4) diff --git a/lib/compass/sass_extensions/sprites/image_group.rb b/lib/compass/sass_extensions/sprites/image_group.rb new file mode 100644 index 00000000..0b251805 --- /dev/null +++ b/lib/compass/sass_extensions/sprites/image_group.rb @@ -0,0 +1,18 @@ +module Compass + module SassExtensions + module Sprites + class ImageGroup + attr_reader :images + + def initialize(images = []) + @images = images + end + + def best_fitting_rows + + end + end + end + end +end + diff --git a/lib/compass/sass_extensions/sprites/image_row.rb b/lib/compass/sass_extensions/sprites/image_row.rb index d7a85d52..d2ffcff5 100644 --- a/lib/compass/sass_extensions/sprites/image_row.rb +++ b/lib/compass/sass_extensions/sprites/image_row.rb @@ -1,8 +1,13 @@ +require 'forwardable' + module Compass module SassExtensions module Sprites class ImageRow + extend Forwardable + attr_reader :images, :max_width + def_delegators :@images, :last, :delete, :empty?, :length def initialize(max_width) @images = [] @@ -10,15 +15,12 @@ module Compass end def add(image) - unless image.is_a?(Compass::SassExtensions::Sprites::Image) - raise ArgumentError, "Must be a SpriteImage" - end - if (images.inject(0) {|sum, img| sum + img.width} + image.width) > max_width - return false - end + return false if !will_fit?(image) @images << image true end + + alias :<< :add def height images.map(&:height).max @@ -27,9 +29,20 @@ module Compass def width images.map(&:width).max end + + def total_width + images.inject(0) {|sum, img| sum + img.width } + end + def efficiency + 1 - (total_width.to_f / max_width.to_f) + end + + def will_fit?(image) + (total_width + image.width) <= max_width + end end end end end - \ No newline at end of file + diff --git a/lib/compass/sass_extensions/sprites/row_fitter.rb b/lib/compass/sass_extensions/sprites/row_fitter.rb new file mode 100644 index 00000000..eaa81bd8 --- /dev/null +++ b/lib/compass/sass_extensions/sprites/row_fitter.rb @@ -0,0 +1,81 @@ +require 'forwardable' + +module Compass + module SassExtensions + module Sprites + class RowFitter + extend Forwardable + + attr_reader :images, :rows + def_delegators :rows, :[] + def initialize(images) + @images = images + @rows = [] + end + + def fit!(style = :scan) + send("#{style}_fit") + end + + def width + @width ||= @images.collect(&:width).max + end + + def efficiency + @rows.inject(0) { |sum, row| sum += row.efficiency } ** @rows.length + end + + private + def new_row(image = nil) + row = Compass::SassExtensions::Sprites::ImageRow.new(width) + row.add(image) if image + row + end + + def fast_fit + row = new_row + @images.each do |image| + if !row.add(image) + @rows << row + row = new_row(image) + end + end + + @rows << row + end + + def scan_fit + fast_fit + + moved_images = [] + + begin + removed = false + + catch :done do + @rows.each do |row| + (@rows - [ row ]).each do |other_row| + other_row.images.each do |image| + if !moved_images.include?(image) + if row.will_fit?(image) + other_row.delete(image) + row << image + + @rows.delete(other_row) if other_row.empty? + removed = true + + moved_images << image + throw :done + end + end + end + end + end + end + end while removed + end + end + end + end +end + diff --git a/test.watchr b/test.watchr new file mode 100644 index 00000000..82613b63 --- /dev/null +++ b/test.watchr @@ -0,0 +1,7 @@ +watch('test/(.*)_test\.rb') { |m| test(m[0]) } +watch('lib/compass/sass_extensions/sprites/image_group.rb') { test('test/units/sprites/image_group_test.rb') } +watch('lib/compass/sass_extensions/sprites/row_fitter.rb') { test('test/units/sprites/row_fitter_test.rb') } + +def test(file = nil) + system %{ruby -I"lib:test" #{file}}.tap { |o| puts o } +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 33dbe6ec..3f13cc8a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -21,7 +21,7 @@ require 'rubygems' if need_gems require 'compass' require 'test/unit' - +require 'mocha' %w(command_line diff io rails test_case).each do |helper| require "helpers/#{helper}" @@ -34,4 +34,4 @@ class Test::Unit::TestCase include Compass::IoHelper extend Compass::TestCaseHelper::ClassMethods -end \ No newline at end of file +end diff --git a/test/units/sprites/image_group_test.rb b/test/units/sprites/image_group_test.rb new file mode 100644 index 00000000..842b11eb --- /dev/null +++ b/test/units/sprites/image_group_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' +require 'compass/sass_extensions/sprites/image_group' + +class ImageGroupTest < Test::Unit::TestCase + def setup + end + +end + diff --git a/test/units/sprites/image_row_test.rb b/test/units/sprites/image_row_test.rb index 397793dc..713d3795 100644 --- a/test/units/sprites/image_row_test.rb +++ b/test/units/sprites/image_row_test.rb @@ -49,4 +49,8 @@ class ImageRowTest < Test::Unit::TestCase assert_equal 40, @image_row.height end -end \ No newline at end of file + it "should have an efficiency rating" do + populate_row + assert_equal 1 - (580.0 / 1000.0), @image_row.efficiency + end +end diff --git a/test/units/sprites/row_fitter_test.rb b/test/units/sprites/row_fitter_test.rb new file mode 100644 index 00000000..d36557b7 --- /dev/null +++ b/test/units/sprites/row_fitter_test.rb @@ -0,0 +1,82 @@ +require 'test_helper' +require 'compass/sass_extensions/sprites/row_fitter' + +class RowFitterTest < Test::Unit::TestCase + def setup + end + + def row_fitter(images = nil) + @row_fitter ||= Compass::SassExtensions::Sprites::RowFitter.new(images) + end + + def teardown + @row_fitter = nil + end + + def create_images(dims) + dims.collect { |width, height| stub(:width => width, :height => height) } + end + + def basic_dims + [ + [ 100, 10 ], + [ 80, 10 ], + [ 50, 10 ], + [ 20, 10 ], + [ 35, 10 ] + ] + end + + it 'should use the fast placement algorithm' do + images = create_images(basic_dims) + + row_fitter(images) + assert_equal 100, row_fitter.width + + row_fitter.fit!(:fast) + + assert_equal 4, row_fitter.rows.length + + assert_equal [ images[0] ], row_fitter[0].images + assert_equal [ images[1] ], row_fitter[1].images + assert_equal [ images[2], images[3] ], row_fitter[2].images + assert_equal [ images[4] ], row_fitter[3].images + end + + it 'should use the scan placement algorithm' do + images = create_images(basic_dims) + + row_fitter(images) + + row_fitter.fit!(:scan) + + assert_equal 3, row_fitter.rows.length + + assert_equal [ images[0] ], row_fitter[0].images + assert_equal [ images[1], images[3] ], row_fitter[1].images + assert_equal [ images[2], images[4] ], row_fitter[2].images + end + + it 'should use the scan placement algorithm and get images from the front' do + images = create_images( + [ + [ 100, 10 ], + [ 35, 10 ], + [ 80, 10 ], + [ 50, 10 ], + [ 20, 10 ], + ] + ) + + row_fitter(images) + + row_fitter.fit!(:scan) + + assert_equal 3, row_fitter.rows.length + + assert_equal [ images[0] ], row_fitter[0].images + assert_equal [ images[1], images[3] ], row_fitter[1].images + assert_equal [ images[2], images[4] ], row_fitter[2].images + end +end +