Skip to content

Commit

Permalink
bin packing
Browse files Browse the repository at this point in the history
  • Loading branch information
Sploder12 committed Nov 6, 2024
1 parent 52d5007 commit 52f87be
Show file tree
Hide file tree
Showing 4 changed files with 302 additions and 2 deletions.
2 changes: 1 addition & 1 deletion src/include/sndx/input/glfw/window_glfw.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ namespace sndx::input {
WindowGLFW& operator=(WindowGLFW&& other) noexcept {
std::swap(m_window, other.m_window);

glfwSetWindowUserPointer(m_window, this);
glfwSetWindowUserPointer(other.m_window, nullptr);
glfwSetWindowUserPointer(m_window, this);
return *this;
}

Expand Down
194 changes: 194 additions & 0 deletions src/include/sndx/math/binpack.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
#pragma once

#include <array>
#include <string>
#include <set>
#include <unordered_map>
#include <vector>
#include <algorithm>
#include <stdexcept>

#include <glm/glm.hpp>

namespace sndx::math {


template <bool horizontal = true, class IdT = std::string>
class BinPacker {
private:
struct Entry {
std::array<size_t, 2> dims{};

constexpr Entry(size_t width, size_t height) noexcept:
dims{ width, height } {}

[[nodiscard]]
constexpr size_t getPrimaryDim() const noexcept {
return dims[horizontal];
}

[[nodiscard]]
constexpr size_t getSecondaryDim() const noexcept {
return dims[!horizontal];
}

constexpr bool operator>(const Entry& other) const noexcept {
if (getPrimaryDim() == other.getPrimaryDim())
return getSecondaryDim() > other.getSecondaryDim();

return getPrimaryDim() > other.getPrimaryDim();
}
};

struct Shelf {
Entry dims{};
size_t occupied{};

std::vector<std::pair<IdT, const Entry*>> entries{};

Shelf(size_t primary, size_t secondary) noexcept :
dims{ primary, secondary } {

if constexpr (horizontal) {
std::swap(dims.dims[0], dims.dims[1]);
}
}

[[nodiscard]]
bool canAddEntry(const Entry& entry) const noexcept {
return occupied + entry.getSecondaryDim() <= dims.getSecondaryDim();
}

void addEntry(const IdT& id, const Entry& entry, size_t padding = 0) noexcept {
occupied += entry.getSecondaryDim() + padding;
entries.emplace_back(id, std::addressof(entry));
}
};

std::multiset<Entry, std::greater<Entry>> m_entries{};
std::unordered_map<IdT, typename decltype(m_entries)::iterator> m_ids{};

public:
bool add(const IdT& id, size_t width, size_t height) {
return m_ids.emplace(id, m_entries.emplace(width, height)).second;
}

bool remove(const IdT& id) {
if (auto itit = m_ids.find(id); itit != m_ids.end()) {
m_entries.erase(itit->second);
m_ids.erase(itit);
return true;
}

return false;
}

struct Packing {
std::unordered_map<IdT, glm::vec<2, size_t>> positions{};
size_t neededWidth{};
size_t neededHeight{};

[[nodiscard]]
auto empty() const noexcept {
return positions.empty();
}

[[nodiscard]]
auto find(const IdT& id) const noexcept {
return positions.find(id);
}

[[nodiscard]]
auto contains(const IdT& id) const noexcept {
return positions.contains(id);
}
};

// uses a modified Next-Fit Decreasing Height/Width algorithm.
[[nodiscard]]
Packing pack(size_t dimConstraint, size_t padding = 0) const {
Packing out{};

if (m_entries.size() == 0) [[unlikely]]
return out;

out.positions.reserve(m_entries.size());

size_t shelfSecondary = dimConstraint;
size_t shelfPrimary = m_entries.cbegin()->getPrimaryDim();

size_t neededPrimary = 0;
size_t neededSecondary = 0;

std::vector<Shelf> shelves{};
Shelf currentShelf{ shelfPrimary, shelfSecondary };

for (const auto& [id, entry_it] : m_ids) {
const auto& entry = *entry_it;

if (entry.getSecondaryDim() > dimConstraint)
throw std::invalid_argument("Cannot pack box that exceeds size constraint itself.");

bool added = false;
for (auto& prevShelf : shelves) {
if (prevShelf.canAddEntry(entry)) {
prevShelf.addEntry(id, entry, padding);
added = true;
}
}

if (!added) {
if (currentShelf.canAddEntry(entry))
currentShelf.addEntry(id, entry, padding);
else {
neededSecondary = std::max(neededSecondary, currentShelf.occupied);
neededPrimary += currentShelf.dims.getPrimaryDim() + padding;
shelves.emplace_back(std::move(currentShelf));
shelfPrimary = entry.getPrimaryDim();
currentShelf = Shelf{ shelfPrimary, shelfSecondary };
currentShelf.addEntry(id, entry, padding);
}
}
}

if (!currentShelf.entries.empty()) {
neededSecondary = std::max(neededSecondary, currentShelf.occupied);
neededPrimary += currentShelf.dims.getPrimaryDim() + padding;
shelves.emplace_back(std::move(currentShelf));
}

neededPrimary -= padding;
neededSecondary -= padding;

if constexpr (horizontal) {
out.neededHeight = neededPrimary;
out.neededWidth = neededSecondary;
}
else {
out.neededWidth = neededPrimary;
out.neededHeight = neededSecondary;
}

size_t primary = 0;
size_t secondary = 0;

for (const auto& shelf : shelves) {
for (auto& [id, entry] : shelf.entries) {
if constexpr (horizontal) {
out.positions.emplace(std::move(id), glm::vec<2, size_t>{secondary, primary});
}
else {
out.positions.emplace(std::move(id), glm::vec<2, size_t>{primary, secondary});
}

secondary += entry->getSecondaryDim() + padding;
}

primary += shelf.dims.getPrimaryDim() + padding;
secondary = 0;
}

return out;
}
};
}
2 changes: 1 addition & 1 deletion src/tests/input/glfw/window_glfw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class GLFWwindowTest : public ::testing::Test {
public:
void SetUp() override {
// it's important to check if we even can test,
// some platforms like GitHub runners can't test GLFW
// headless platforms like GitHub runners can't test GLFW
if (glfwInit() != GLFW_TRUE) {
const char* what = "";
glfwGetError(&what);
Expand Down
106 changes: 106 additions & 0 deletions src/tests/math/binpack.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#include "math/binpack.hpp"

#include <gtest/gtest.h>

using namespace sndx::math;

TEST(Binpack, TrivialPacking) {
BinPacker packer{};

auto none_out = packer.pack(0);
EXPECT_TRUE(none_out.empty());
EXPECT_EQ(none_out.neededHeight, 0);
EXPECT_EQ(none_out.neededWidth, 0);

ASSERT_TRUE(packer.add("a", 10, 5));

auto trivial_out = packer.pack(10);

EXPECT_EQ(trivial_out.neededHeight, 5);
EXPECT_EQ(trivial_out.neededWidth, 10);
ASSERT_TRUE(trivial_out.contains("a"));

EXPECT_EQ(trivial_out.positions["a"].x, 0);
EXPECT_EQ(trivial_out.positions["a"].y, 0);

ASSERT_TRUE(packer.add("b", 1, 5));
auto horizontal_out = packer.pack(11);

EXPECT_EQ(horizontal_out.neededHeight, 5);
EXPECT_EQ(horizontal_out.neededWidth, 11);

EXPECT_TRUE(horizontal_out.contains("a"));
ASSERT_TRUE(horizontal_out.contains("b"));

EXPECT_EQ(horizontal_out.positions["a"].y, 0);
EXPECT_EQ(horizontal_out.positions["b"].y, 0);

auto aX = horizontal_out.positions["a"].x;
auto bX = horizontal_out.positions["b"].x;

EXPECT_NE(aX, bX);
EXPECT_TRUE(aX == 0 || aX == 1);
EXPECT_TRUE(bX == 0 || bX == 10);


BinPacker<false> verticalPacker{};

ASSERT_TRUE(verticalPacker.add("a", 5, 10));
ASSERT_TRUE(verticalPacker.add("b", 5, 1));
auto vertical_out = verticalPacker.pack(11);

EXPECT_EQ(vertical_out.neededHeight, 11);
EXPECT_EQ(vertical_out.neededWidth, 5);

EXPECT_TRUE(vertical_out.contains("a"));
ASSERT_TRUE(vertical_out.contains("b"));

EXPECT_EQ(vertical_out.positions["a"].x, 0);
EXPECT_EQ(vertical_out.positions["b"].x, 0);

auto aY = vertical_out.positions["a"].y;
auto bY = vertical_out.positions["b"].y;

EXPECT_NE(aY, bY);
EXPECT_TRUE(aY == 0 || aY == 1);
EXPECT_TRUE(bY == 0 || bY == 10);
}

TEST(Binpack, PaddingPads) {
BinPacker packer{};

auto none_out = packer.pack(0, 20);
EXPECT_TRUE(none_out.empty());
EXPECT_EQ(none_out.neededHeight, 0);
EXPECT_EQ(none_out.neededWidth, 0);

ASSERT_TRUE(packer.add("a", 10, 5));

auto trivial_out = packer.pack(10, 20);

EXPECT_EQ(trivial_out.neededHeight, 5);
EXPECT_EQ(trivial_out.neededWidth, 10);
ASSERT_TRUE(trivial_out.contains("a"));

EXPECT_EQ(trivial_out.positions["a"].x, 0);
EXPECT_EQ(trivial_out.positions["a"].y, 0);

ASSERT_TRUE(packer.add("b", 1, 5));
auto horizontal_out = packer.pack(11 + 20, 20);

EXPECT_EQ(horizontal_out.neededHeight, 5);
EXPECT_EQ(horizontal_out.neededWidth, 11 + 20);

EXPECT_TRUE(horizontal_out.contains("a"));
ASSERT_TRUE(horizontal_out.contains("b"));

EXPECT_EQ(horizontal_out.positions["a"].y, 0);
EXPECT_EQ(horizontal_out.positions["b"].y, 0);

auto aX = horizontal_out.positions["a"].x;
auto bX = horizontal_out.positions["b"].x;

EXPECT_NE(aX, bX);
EXPECT_TRUE(aX == 0 || aX == 1 + 20);
EXPECT_TRUE(bX == 0 || bX == 10 + 20);
}

0 comments on commit 52f87be

Please sign in to comment.