2023-11-09 22:28:02 +04:00

536 lines
15 KiB
C++

#ifndef SHELF_PACK_HPP
#define SHELF_PACK_HPP
#include <algorithm>
#include <cstdint>
#include <deque>
#include <limits>
#include <map>
#include <vector>
namespace mapbox {
const char * const SHELF_PACK_VERSION = "2.1.1";
class Bin {
friend class ShelfPack;
public:
/**
* Create a new Bin.
*
* @class Bin
* @param {int32_t} id Unique bin identifier
* @param {int32_t} [w1=-1] Width of the new Bin
* @param {int32_t} [h1=-1] Height of the new Bin
* @param {int32_t} [maxw1=-1] Maximum Width of the new Bin
* @param {int32_t} [maxh1=-1] Maximum Height of the new Bin
* @param {int32_t} [x1=-1] X location of the Bin
* @param {int32_t} [y1=-1] Y location of the Bin
*
* @example
* Bin b(-1, 12, 16);
*/
explicit Bin(
int32_t id1 = -1,
int32_t w1 = -1,
int32_t h1 = -1,
int32_t maxw1 = -1,
int32_t maxh1 = -1,
int32_t x1 = -1,
int32_t y1 = -1
) : id(id1), w(w1), h(h1), maxw(maxw1), maxh(maxh1), x(x1), y(y1), refcount_(0) {
if (maxw == -1) {
maxw = w;
}
if (maxh == -1) {
maxh = h;
}
}
int32_t id;
int32_t w;
int32_t h;
int32_t maxw;
int32_t maxh;
int32_t x;
int32_t y;
int32_t refcount() const { return refcount_; }
private:
int32_t refcount_;
};
class Shelf {
public:
/**
* Create a new Shelf.
*
* @class Shelf
* @param {int32_t} y1 Top coordinate of the new shelf
* @param {int32_t} w1 Width of the new shelf
* @param {int32_t} h1 Height of the new shelf
*
* @example
* Shelf shelf(64, 512, 24);
*/
explicit Shelf(int32_t y1, int32_t w1, int32_t h1) :
x_(0), y_(y1), w_(w1), h_(h1), wfree_(w1) { }
/**
* Allocate a single bin into the shelf.
* Bin is stored in a `bins_` container.
* Returned pointer is stable until the shelf is destroyed.
*
* @param {int32_t} id Unique bin identifier, pass -1 to generate a new one
* @param {int32_t} w1 Width of the bin to allocate
* @param {int32_t} h1 Height of the bin to allocate
* @returns {Bin*} `Bin` pointer with `id`, `x`, `y`, `w`, `h` members
*
* @example
* Bin* result = shelf.alloc(-1, 12, 16);
*/
Bin* alloc(int32_t id, int32_t w1, int32_t h1) {
if (w1 > wfree_ || h1 > h_) {
return nullptr;
}
int32_t x1 = x_;
x_ += w1;
wfree_ -= w1;
bins_.emplace_back(id, w1, h1, w1, h_, x1, y_);
return &bins_.back();
}
/**
* Resize the shelf.
*
* @param {int32_t} w1 Requested new width of the shelf
* @returns {bool} `true` if resize succeeded, `false` if failed
*
* @example
* shelf.resize(512);
*/
bool resize(int32_t w1) {
wfree_ += (w1 - w_);
w_ = w1;
return true;
}
int32_t x() const { return x_; }
int32_t y() const { return y_; }
int32_t w() const { return w_; }
int32_t h() const { return h_; }
int32_t wfree() const { return wfree_; }
private:
int32_t x_;
int32_t y_;
int32_t w_;
int32_t h_;
int32_t wfree_;
std::deque<Bin> bins_;
};
class ShelfPack {
public:
struct ShelfPackOptions {
inline ShelfPackOptions() : autoResize(false) { };
bool autoResize;
};
struct PackOptions {
inline PackOptions() : inPlace(false) { };
bool inPlace;
};
/**
* Create a new ShelfPack bin allocator.
*
* Uses the Shelf Best Height Fit algorithm from
* http://clb.demon.fi/files/RectangleBinPack.pdf
*
* @class ShelfPack
* @param {int32_t} [w=64] Initial width of the sprite
* @param {int32_t} [h=64] Initial width of the sprite
* @param {ShelfPackOptions} [options]
* @param {bool} [options.autoResize=false] If `true`, the sprite will automatically grow
*
* @example
* ShelfPack::ShelfPackOptions options;
* options.autoResize = false;
* ShelfPack sprite = new ShelfPack(64, 64, options);
*/
explicit ShelfPack(int32_t w = 0, int32_t h = 0, const ShelfPackOptions &options = ShelfPackOptions{}) {
width_ = w > 0 ? w : 64;
height_ = h > 0 ? h : 64;
autoResize_ = options.autoResize;
maxId_ = 0;
}
/**
* Batch pack multiple bins into the sprite.
*
* @param {vector<Bin>} bins Array of requested bins - each object should have `w`, `h` values
* @param {PackOptions} [options]
* @param {bool} [options.inPlace=false] If `true`, the supplied bin objects will be updated inplace with `x` and `y` values
* @returns {vector<Bin*>} Array of Bin pointers - each bin is a struct with `x`, `y`, `w`, `h` values
*
* @example
* std::vector<Bin> moreBins;
* moreBins.emplace_back(-1, 12, 24);
* moreBins.emplace_back(-1, 12, 12);
* moreBins.emplace_back(-1, 10, 10);
*
* ShelfPack::PackOptions options;
* options.inPlace = true;
* std::vector<Bin*> results = sprite.pack(moreBins, options);
*/
std::vector<Bin*> pack(std::vector<Bin> &bins, const PackOptions &options = PackOptions{}) {
std::vector<Bin*> results;
for (auto& bin : bins) {
if (bin.w > 0 && bin.h > 0) {
Bin* allocation = packOne(bin.id, bin.w, bin.h);
if (!allocation) {
continue;
}
if (options.inPlace) {
bin.id = allocation->id;
bin.x = allocation->x;
bin.y = allocation->y;
}
results.push_back(allocation);
}
}
shrink();
return results;
}
/**
* Pack a single bin into the sprite.
*
* @param {int32_t} id Unique bin identifier, pass -1 to generate a new one
* @param {int32_t} w Width of the bin to allocate
* @param {int32_t} h Height of the bin to allocate
* @returns {Bin*} Pointer to a packed Bin with `id`, `x`, `y`, `w`, `h` members
*
* @example
* Bin* result = sprite.packOne(-1, 12, 16);
*/
Bin* packOne(int32_t id, int32_t w, int32_t h) {
int32_t y = 0;
int32_t waste = 0;
struct {
Shelf* pshelf = nullptr;
Bin* pfreebin = nullptr;
int32_t waste = std::numeric_limits<std::int32_t>::max();
} best;
// if id was supplied, attempt a lookup..
if (id != -1) {
Bin* pbin = getBin(id);
if (pbin) { // we packed this bin already
ref(*pbin);
return pbin;
}
maxId_ = std::max(id, maxId_);
} else {
id = ++maxId_;
}
// First try to reuse a free bin..
for (auto& freebin : freebins_) {
// exactly the right height and width, use it..
if (h == freebin->maxh && w == freebin->maxw) {
return allocFreebin(freebin, id, w, h);
}
// not enough height or width, skip it..
if (h > freebin->maxh || w > freebin->maxw) {
continue;
}
// extra height or width, minimize wasted area..
if (h <= freebin->maxh && w <= freebin->maxw) {
waste = (freebin->maxw * freebin->maxh) - (w * h);
if (waste < best.waste) {
best.waste = waste;
best.pfreebin = freebin;
}
}
}
// Next find the best shelf
for (auto& shelf : shelves_) {
y += shelf.h();
// not enough width on this shelf, skip it..
if (w > shelf.wfree()) {
continue;
}
// exactly the right height, pack it..
if (h == shelf.h()) {
return allocShelf(shelf, id, w, h);
}
// not enough height, skip it..
if (h > shelf.h()) {
continue;
}
// extra height, minimize wasted area..
if (h < shelf.h()) {
waste = (shelf.h() - h) * w;
if (waste < best.waste) {
best.waste = waste;
best.pshelf = &shelf;
}
}
}
if (best.pfreebin) {
return allocFreebin(best.pfreebin, id, w, h);
}
if (best.pshelf) {
return allocShelf(*best.pshelf, id, w, h);
}
// No free bins or shelves.. add shelf..
if (h <= (height_ - y) && w <= width_) {
shelves_.emplace_back(y, width_, h);
return allocShelf(shelves_.back(), id, w, h);
}
// No room for more shelves..
// If `autoResize` option is set, grow the sprite as follows:
// * double whichever sprite dimension is smaller (`w1` or `h1`)
// * if sprite dimensions are equal, grow width before height
// * accomodate very large bin requests (big `w` or `h`)
if (autoResize_) {
int32_t h1, h2, w1, w2;
h1 = h2 = height_;
w1 = w2 = width_;
if (w1 <= h1 || w > w1) { // grow width..
w2 = std::max(w, w1) * 2;
}
if (h1 < w1 || h > h1) { // grow height..
h2 = std::max(h, h1) * 2;
}
resize(w2, h2);
return packOne(id, w, h); // retry
}
return nullptr;
}
/**
*
* Shrink the width/height of the sprite to the bare minimum.
* Since shelf-pack doubles first width, then height when running out of shelf space
* this can result in fairly large unused space both in width and height if that happens
* towards the end of bin packing.
*/
void shrink() {
if (shelves_.size()) {
int32_t w2 = 0;
int32_t h2 = 0;
for (auto& shelf : shelves_) {
h2 += shelf.h();
w2 = std::max(shelf.w() - shelf.wfree(), w2);
}
resize(w2, h2);
}
}
/**
* Return a packed bin given its id, or nullptr if the id is not found
*
* @param {int32_t} id Unique identifier for this bin,
* @returns {Bin*} Pointer to a packed Bin with `id`, `x`, `y`, `w`, `h` members
*
* @example
* Bin* result = sprite.getBin(5);
*/
Bin* getBin(int32_t id) {
std::map<int32_t, Bin*>::iterator it = usedbins_.find(id);
return (it == usedbins_.end()) ? nullptr : it->second;
}
/**
* Increment the ref count of a bin and update statistics.
*
* @param {Bin&} bin Bin reference
* @returns {int32_t} New refcount of the bin
*
* @example
* Bin* bin = sprite.getBin(5);
* if (bin) {
* sprite.ref(*bin);
* }
*/
int32_t ref(Bin& bin) {
if (++bin.refcount_ == 1) { // a new Bin.. record height in stats historgram..
int32_t h = bin.h;
stats_[h] = (stats_[h] | 0) + 1;
}
return bin.refcount_;
};
/**
* Decrement the ref count of a bin and update statistics.
* The bin will be automatically marked as free space once the refcount reaches 0.
* Memory for the bin is not freed, as unreferenced bins may be reused later.
*
* @param {Bin&} bin Bin reference
* @returns {int32_t} New refcount of the bin
*
* @example
* Bin* bin = sprite.getBin(5);
* if (bin) {
* sprite.unref(*bin);
* }
*/
int32_t unref(Bin& bin) {
if (bin.refcount_ == 0) {
return 0;
}
if (--bin.refcount_ == 0) {
stats_[bin.h]--;
usedbins_.erase(bin.id);
freebins_.push_back(&bin);
}
return bin.refcount_;
}
/**
* Clear the sprite and reset statistics.
*
* @example
* sprite.clear();
*/
void clear() {
shelves_.clear();
freebins_.clear();
usedbins_.clear();
stats_.clear();
maxId_ = 0;
}
/**
* Resize the sprite.
*
* @param {int32_t} w Requested new sprite width
* @param {int32_t} h Requested new sprite height
* @returns {bool} `true` if resize succeeded, `false` if failed
*
* @example
* sprite.resize(256, 256);
*/
bool resize(int32_t w, int32_t h) {
width_ = w;
height_ = h;
for (auto& shelf : shelves_) {
shelf.resize(width_);
}
return true;
}
int32_t width() const { return width_; }
int32_t height() const { return height_; }
private:
/**
* Called by packOne() to allocate a bin by reusing an existing freebin
*
* @private
* @param {Bin*} bin Pointer to a freebin to reuse
* @param {int32_t} w Width of the bin to allocate
* @param {int32_t} h Height of the bin to allocate
* @param {int32_t} id Unique identifier for this bin
* @returns {Bin*} Pointer to a Bin with `id`, `x`, `y`, `w`, `h` properties
*
* @example
* Bin* bin = sprite.allocFreebin(pfreebin, 12, 16, 5);
*/
Bin* allocFreebin(Bin* bin, int32_t id, int32_t w, int32_t h) {
freebins_.erase(std::remove(freebins_.begin(), freebins_.end(), bin), freebins_.end());
bin->id = id;
bin->w = w;
bin->h = h;
bin->refcount_ = 0;
usedbins_[id] = bin;
ref(*bin);
return bin;
}
/**
* Called by `packOne() to allocate bin on an existing shelf
* Memory for the bin is allocated on the heap by `shelf.alloc()`
*
* @private
* @param {Shelf&} shelf Reference to the shelf to allocate the bin on
* @param {int32_t} w Width of the bin to allocate
* @param {int32_t} h Height of the bin to allocate
* @param {int32_t} id Unique identifier for this bin
* @returns {Bin*} Pointer to a Bin with `id`, `x`, `y`, `w`, `h` properties
*
* @example
* Bin* bin = sprite.allocShelf(shelf, 12, 16, 5);
*/
Bin* allocShelf(Shelf& shelf, int32_t id, int32_t w, int32_t h) {
Bin* pbin = shelf.alloc(id, w, h);
if (pbin) {
usedbins_[id] = pbin;
ref(*pbin);
}
return pbin;
}
int32_t width_;
int32_t height_;
int32_t maxId_;
bool autoResize_;
std::deque<Shelf> shelves_;
std::map<int32_t, Bin*> usedbins_;
std::vector<Bin*> freebins_;
std::map<int32_t, int32_t> stats_;
};
} // namespace mapbox
#endif