mirror of
https://github.com/gbdev/rgbds.git
synced 2025-11-20 10:12:06 +00:00
573 lines
19 KiB
C++
573 lines
19 KiB
C++
// SPDX-License-Identifier: MIT
|
|
|
|
#include "gfx/pal_packing.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <deque>
|
|
#include <inttypes.h>
|
|
#include <iterator>
|
|
#include <numeric>
|
|
#include <optional>
|
|
#include <queue>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <type_traits>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "helpers.hpp"
|
|
#include "style.hpp"
|
|
#include "verbosity.hpp"
|
|
|
|
#include "gfx/color_set.hpp"
|
|
#include "gfx/main.hpp"
|
|
|
|
// The solvers here are picked from the paper at https://arxiv.org/abs/1605.00558:
|
|
// "Algorithms for the Pagination Problem, a Bin Packing with Overlapping Items"
|
|
// Their formulation of the problem consists in packing "tiles" into "pages".
|
|
// Here is a correspondence table for our application of it:
|
|
//
|
|
// Paper | RGBGFX
|
|
// -------+----------
|
|
// Symbol | Color
|
|
// Tile | Color set
|
|
// Page | Palette
|
|
|
|
// A reference to a color set, and attached attributes for sorting purposes
|
|
struct ColorSetAttrs {
|
|
size_t colorSetIndex;
|
|
// Pages from which we are banned (to prevent infinite loops)
|
|
// This is dynamic because we wish not to hard-cap the amount of palettes
|
|
std::vector<bool> bannedPages;
|
|
|
|
explicit ColorSetAttrs(size_t index) : colorSetIndex(index) {}
|
|
|
|
bool isBannedFrom(size_t index) const {
|
|
return index < bannedPages.size() && bannedPages[index];
|
|
}
|
|
|
|
void banFrom(size_t index) {
|
|
if (bannedPages.size() <= index) {
|
|
bannedPages.resize(index + 1);
|
|
}
|
|
bannedPages[index] = true;
|
|
}
|
|
};
|
|
|
|
// A collection of color sets assigned to a palette
|
|
// Does not contain the actual color indices because we need to be able to remove elements
|
|
class AssignedSets {
|
|
// We leave room for emptied slots to avoid copying the structs around on removal
|
|
std::vector<std::optional<ColorSetAttrs>> _assigned;
|
|
// For resolving color set indices
|
|
std::vector<ColorSet> const *_colorSets;
|
|
|
|
public:
|
|
AssignedSets(std::vector<ColorSet> const &colorSets, std::optional<ColorSetAttrs> &&attrs)
|
|
: _assigned{attrs}, _colorSets{&colorSets} {}
|
|
|
|
private:
|
|
// Template class for both const and non-const iterators over the non-empty `_assigned` slots
|
|
template<typename IteratorT, template<typename> typename Constness>
|
|
class AssignedSetsIter {
|
|
public:
|
|
friend class AssignedSets;
|
|
|
|
// For `iterator_traits`
|
|
using value_type = ColorSetAttrs;
|
|
using difference_type = ptrdiff_t;
|
|
using reference = Constness<value_type> &;
|
|
using pointer = Constness<value_type> *;
|
|
using iterator_category = std::forward_iterator_tag;
|
|
|
|
private:
|
|
Constness<decltype(_assigned)> *_array = nullptr;
|
|
IteratorT _iter{};
|
|
|
|
AssignedSetsIter(decltype(_array) array, decltype(_iter) &&iter)
|
|
: _array(array), _iter(iter) {}
|
|
|
|
AssignedSetsIter &skipEmpty() {
|
|
while (_iter != _array->end() && !_iter->has_value()) {
|
|
++_iter;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
public:
|
|
AssignedSetsIter() = default;
|
|
|
|
bool operator==(AssignedSetsIter const &rhs) const { return _iter == rhs._iter; }
|
|
|
|
AssignedSetsIter &operator++() {
|
|
++_iter;
|
|
skipEmpty();
|
|
return *this;
|
|
}
|
|
AssignedSetsIter operator++(int) {
|
|
AssignedSetsIter it = *this;
|
|
++(*this);
|
|
return it;
|
|
}
|
|
|
|
reference operator*() const {
|
|
assume((*_iter).has_value());
|
|
return **_iter;
|
|
}
|
|
pointer operator->() const {
|
|
return &(**this); // Invokes the operator above, not quite a no-op!
|
|
}
|
|
|
|
friend void swap(AssignedSetsIter &lhs, AssignedSetsIter &rhs) {
|
|
std::swap(lhs._array, rhs._array);
|
|
std::swap(lhs._iter, rhs._iter);
|
|
}
|
|
};
|
|
|
|
public:
|
|
using iterator = AssignedSetsIter<decltype(_assigned)::iterator, std::remove_const_t>;
|
|
iterator begin() { return iterator{&_assigned, _assigned.begin()}.skipEmpty(); }
|
|
iterator end() { return iterator{&_assigned, _assigned.end()}; }
|
|
|
|
using const_iterator = AssignedSetsIter<decltype(_assigned)::const_iterator, std::add_const_t>;
|
|
const_iterator begin() const {
|
|
return const_iterator{&_assigned, _assigned.begin()}.skipEmpty();
|
|
}
|
|
const_iterator end() const { return const_iterator{&_assigned, _assigned.end()}; }
|
|
|
|
void assign(ColorSetAttrs const &&attrs) {
|
|
auto freeSlot =
|
|
std::find_if_not(RANGE(_assigned), [](std::optional<ColorSetAttrs> const &slot) {
|
|
return slot.has_value();
|
|
});
|
|
if (freeSlot == _assigned.end()) {
|
|
_assigned.emplace_back(attrs); // We are full, use a new slot
|
|
} else {
|
|
freeSlot->emplace(attrs); // Reuse a free slot
|
|
}
|
|
}
|
|
|
|
void remove(iterator const &iter) {
|
|
iter._iter->reset(); // This time, we want to access the `optional` itself
|
|
}
|
|
|
|
void clear() { _assigned.clear(); }
|
|
|
|
bool empty() const {
|
|
return std::none_of(RANGE(_assigned), [](std::optional<ColorSetAttrs> const &slot) {
|
|
return slot.has_value();
|
|
});
|
|
}
|
|
|
|
size_t nbColorSets() const { return std::distance(RANGE(*this)); }
|
|
|
|
private:
|
|
template<typename IteratorT>
|
|
static void addUniqueColors(
|
|
std::unordered_set<uint16_t> &colors,
|
|
IteratorT iter,
|
|
IteratorT const &end,
|
|
std::vector<ColorSet> const &colorSets
|
|
) {
|
|
for (; iter != end; ++iter) {
|
|
ColorSet const &colorSet = colorSets[iter->colorSetIndex];
|
|
colors.insert(RANGE(colorSet));
|
|
}
|
|
}
|
|
|
|
public:
|
|
// Returns the set of distinct colors
|
|
std::unordered_set<uint16_t> uniqueColors() const {
|
|
std::unordered_set<uint16_t> colors;
|
|
addUniqueColors(colors, RANGE(*this), *_colorSets);
|
|
return colors;
|
|
}
|
|
|
|
// Returns the number of distinct colors
|
|
size_t volume() const { return uniqueColors().size(); }
|
|
|
|
bool canFit(ColorSet const &colorSet) const {
|
|
std::unordered_set<uint16_t> colors = uniqueColors();
|
|
colors.insert(RANGE(colorSet));
|
|
return colors.size() <= options.maxOpaqueColors();
|
|
}
|
|
|
|
// Counts how many of our color sets this color also belongs to
|
|
uint32_t multiplicity(uint16_t color) const {
|
|
return std::count_if(RANGE(*this), [this, &color](ColorSetAttrs const &attrs) {
|
|
ColorSet const &pal = (*_colorSets)[attrs.colorSetIndex];
|
|
return std::find(RANGE(pal), color) != pal.end();
|
|
});
|
|
}
|
|
|
|
// The `relSizeOf` method below should compute the sum, for each color in `colorSet`, of
|
|
// the reciprocal of the "multiplicity" of the color across "our" color sets.
|
|
// However, literally computing the reciprocals would involve floating-point division, which
|
|
// leads to imprecision and even platform-specific differences.
|
|
// We avoid this by multiplying the reciprocals by a factor such that division always produces
|
|
// an integer; the LCM of all values the denominator can take is the smallest suitable factor.
|
|
static constexpr uint32_t scaleFactor = [] {
|
|
// Fold over 1..=17 with the associative LCM function
|
|
// (17 is the largest the denominator in `relSizeOf` below can be)
|
|
uint32_t factor = 1;
|
|
for (uint32_t n = 2; n <= 17; ++n) {
|
|
factor = std::lcm(factor, n);
|
|
}
|
|
return factor;
|
|
}();
|
|
|
|
// Computes the "relative size" of a color set on this palette;
|
|
// it's a measure of how much this color set would "cost" to introduce.
|
|
uint32_t relSizeOf(ColorSet const &colorSet) const {
|
|
uint32_t relSize = 0;
|
|
for (uint16_t color : colorSet) {
|
|
uint32_t n = multiplicity(color);
|
|
// We increase the denominator by 1 here; the reference code does this,
|
|
// but the paper does not. Not adding 1 makes a multiplicity of 0 cause a division by 0
|
|
// (that is, if the color is not found in any color set), and adding 1 still seems
|
|
// to preserve the paper's reasoning.
|
|
//
|
|
// The scale factor should ensure integer divisions only.
|
|
assume(scaleFactor % (n + 1) == 0);
|
|
relSize += scaleFactor / (n + 1);
|
|
}
|
|
return relSize;
|
|
}
|
|
|
|
// Computes the "relative size" of a set of color sets on this palette
|
|
template<typename IteratorT>
|
|
size_t combinedVolume(
|
|
IteratorT &&begin, IteratorT const &end, std::vector<ColorSet> const &colorSets
|
|
) const {
|
|
std::unordered_set<uint16_t> colors = uniqueColors();
|
|
addUniqueColors(colors, std::forward<IteratorT>(begin), end, colorSets);
|
|
return colors.size();
|
|
}
|
|
|
|
// Computes the "relative size" of a set of colors on this palette
|
|
template<typename IteratorT>
|
|
size_t combinedVolume(IteratorT &&begin, IteratorT &&end) const {
|
|
std::unordered_set<uint16_t> colors = uniqueColors();
|
|
colors.insert(std::forward<IteratorT>(begin), std::forward<IteratorT>(end));
|
|
return colors.size();
|
|
}
|
|
};
|
|
|
|
// LCOV_EXCL_START
|
|
static void verboseOutputAssignments(
|
|
std::vector<AssignedSets> const &assignments, std::vector<ColorSet> const &colorSets
|
|
) {
|
|
if (!checkVerbosity(VERB_INFO)) {
|
|
return;
|
|
}
|
|
|
|
style_Set(stderr, STYLE_MAGENTA, false);
|
|
for (AssignedSets const &assignment : assignments) {
|
|
fputs("{ ", stderr);
|
|
for (ColorSetAttrs const &attrs : assignment) {
|
|
fprintf(stderr, "[%zu] ", attrs.colorSetIndex);
|
|
for (uint16_t colorIndex : colorSets[attrs.colorSetIndex]) {
|
|
fprintf(stderr, "%04" PRIx16 ", ", colorIndex);
|
|
}
|
|
}
|
|
fprintf(stderr, "} (volume = %zu)\n", assignment.volume());
|
|
}
|
|
style_Reset(stderr);
|
|
}
|
|
// LCOV_EXCL_STOP
|
|
|
|
static void decant(std::vector<AssignedSets> &assignments, std::vector<ColorSet> const &colorSets) {
|
|
// "Decanting" is the process of moving all *things* that can fit in a lower index there
|
|
auto decantOn = [&assignments](auto const &tryDecanting) {
|
|
// No need to attempt decanting on palette #0, as there are no palettes to decant to
|
|
for (size_t from = assignments.size(); --from;) {
|
|
// Scan all palettes before this one
|
|
for (size_t to = 0; to < from; ++to) {
|
|
tryDecanting(assignments[to], assignments[from]);
|
|
}
|
|
|
|
// If the color set is now empty, remove it
|
|
// Doing this now reduces the number of iterations performed by later steps
|
|
// NB: order is intentionally preserved so as not to alter the "decantation"'s
|
|
// properties
|
|
// NB: this does mean that the first step might get empty palettes as its input!
|
|
// NB: this is safe to do because we go towards the beginning of the vector, thereby not
|
|
// invalidating our iteration (thus, iterators should not be used to drivethe outer
|
|
// loop)
|
|
if (assignments[from].empty()) {
|
|
assignments.erase(assignments.begin() + from);
|
|
}
|
|
}
|
|
};
|
|
|
|
verbosePrint(VERB_DEBUG, "%zu palettes before decanting\n", assignments.size());
|
|
|
|
// Decant on palettes
|
|
decantOn([&colorSets](AssignedSets &to, AssignedSets &from) {
|
|
// If the entire palettes can be merged, move all of `from`'s color sets
|
|
if (to.combinedVolume(RANGE(from), colorSets) <= options.maxOpaqueColors()) {
|
|
for (ColorSetAttrs &attrs : from) {
|
|
to.assign(std::move(attrs));
|
|
}
|
|
from.clear();
|
|
}
|
|
});
|
|
verbosePrint(VERB_DEBUG, "%zu palettes after decanting on palettes\n", assignments.size());
|
|
|
|
// Decant on "components" (color sets sharing colors)
|
|
decantOn([&colorSets](AssignedSets &to, AssignedSets &from) {
|
|
// We need to iterate on all the "components", which are groups of color sets sharing at
|
|
// least one color with another color set in the group.
|
|
// We do this by adding the first available color set, and then looking for palettes with
|
|
// common colors. (As an optimization, we know we can skip palettes already scanned.)
|
|
std::vector<bool> processed(from.nbColorSets(), false);
|
|
for (std::vector<bool>::iterator wasProcessed;
|
|
(wasProcessed = std::find(RANGE(processed), false)) != processed.end();) {
|
|
auto attrs = from.begin();
|
|
std::advance(attrs, wasProcessed - processed.begin());
|
|
|
|
std::unordered_set<uint16_t> colors(RANGE(colorSets[attrs->colorSetIndex]));
|
|
std::vector<size_t> members = {static_cast<size_t>(wasProcessed - processed.begin())};
|
|
*wasProcessed = true; // Mark the first color set as processed
|
|
|
|
// Build up the "component"...
|
|
for (; ++wasProcessed != processed.end(); ++attrs) {
|
|
// If at least one color matches, add it
|
|
if (ColorSet const &colorSet = colorSets[attrs->colorSetIndex];
|
|
std::find_first_of(RANGE(colors), RANGE(colorSet)) != colors.end()) {
|
|
colors.insert(RANGE(colorSet));
|
|
members.push_back(wasProcessed - processed.begin());
|
|
*wasProcessed = true; // Mark that color set as processed
|
|
}
|
|
}
|
|
|
|
if (to.combinedVolume(RANGE(colors)) > options.maxOpaqueColors()) {
|
|
continue;
|
|
}
|
|
|
|
// Iterate through the component's color sets, and transfer them
|
|
auto member = from.begin();
|
|
size_t curIndex = 0;
|
|
for (size_t index : members) {
|
|
std::advance(member, index - curIndex);
|
|
curIndex = index;
|
|
to.assign(std::move(*member));
|
|
from.remove(member); // Removing does not shift elements, so it's cheap
|
|
}
|
|
}
|
|
});
|
|
verbosePrint(
|
|
VERB_DEBUG, "%zu palettes after decanting on \"components\"\n", assignments.size()
|
|
);
|
|
|
|
// Decant on individual color sets
|
|
decantOn([&colorSets](AssignedSets &to, AssignedSets &from) {
|
|
for (auto it = from.begin(); it != from.end(); ++it) {
|
|
if (to.canFit(colorSets[it->colorSetIndex])) {
|
|
to.assign(std::move(*it));
|
|
from.remove(it);
|
|
}
|
|
}
|
|
});
|
|
verbosePrint(VERB_DEBUG, "%zu palettes after decanting on color sets\n", assignments.size());
|
|
}
|
|
|
|
std::pair<std::vector<size_t>, size_t> overloadAndRemove(std::vector<ColorSet> const &colorSets) {
|
|
verbosePrint(VERB_NOTICE, "Paginating palettes using \"overload-and-remove\" strategy...\n");
|
|
|
|
// Sort the color sets by size, which improves the packing algorithm's efficiency
|
|
auto const indexOfLargestColorSetFirst = [&colorSets](size_t left, size_t right) {
|
|
ColorSet const &lhs = colorSets[left];
|
|
ColorSet const &rhs = colorSets[right];
|
|
return lhs.size() > rhs.size(); // We want the color sets to be sorted *largest first*!
|
|
};
|
|
std::vector<size_t> sortedColorSetIDs;
|
|
sortedColorSetIDs.reserve(colorSets.size());
|
|
for (size_t i = 0; i < colorSets.size(); ++i) {
|
|
sortedColorSetIDs.insert(
|
|
std::lower_bound(RANGE(sortedColorSetIDs), i, indexOfLargestColorSetFirst), i
|
|
);
|
|
}
|
|
|
|
// Begin with no pages
|
|
std::vector<AssignedSets> assignments;
|
|
|
|
// Begin with all color sets queued up for insertion
|
|
for (std::queue<ColorSetAttrs> queue(std::deque<ColorSetAttrs>(RANGE(sortedColorSetIDs)));
|
|
!queue.empty();
|
|
queue.pop()) {
|
|
ColorSetAttrs const &attrs = queue.front(); // Valid until the `queue.pop()`
|
|
verbosePrint(VERB_TRACE, "Handling color set %zu\n", attrs.colorSetIndex);
|
|
|
|
ColorSet const &colorSet = colorSets[attrs.colorSetIndex];
|
|
size_t bestPalIndex = assignments.size();
|
|
// We're looking for a palette where the color set's relative size is less than
|
|
// its actual size; so only overwrite the "not found" index on meeting that criterion
|
|
uint32_t bestRelSize = colorSet.size() * AssignedSets::scaleFactor;
|
|
|
|
for (size_t i = 0; i < assignments.size(); ++i) {
|
|
// Skip the page if this one is banned from it
|
|
if (attrs.isBannedFrom(i)) {
|
|
continue;
|
|
}
|
|
|
|
uint32_t relSize = assignments[i].relSizeOf(colorSet);
|
|
verbosePrint(
|
|
VERB_TRACE,
|
|
" Relative size to palette %zu (of %zu): %" PRIu32 " (size = %zu)\n",
|
|
i,
|
|
assignments.size(),
|
|
relSize,
|
|
colorSet.size()
|
|
);
|
|
if (relSize < bestRelSize) {
|
|
bestPalIndex = i;
|
|
bestRelSize = relSize;
|
|
}
|
|
}
|
|
|
|
if (bestPalIndex == assignments.size()) {
|
|
// Found nowhere to put it, create a new page containing just that one
|
|
verbosePrint(
|
|
VERB_TRACE,
|
|
"Assigning color set %zu to new palette %zu\n",
|
|
attrs.colorSetIndex,
|
|
bestPalIndex
|
|
);
|
|
assignments.emplace_back(colorSets, std::move(attrs));
|
|
continue;
|
|
}
|
|
|
|
verbosePrint(
|
|
VERB_TRACE,
|
|
"Assigning color set %zu to palette %zu\n",
|
|
attrs.colorSetIndex,
|
|
bestPalIndex
|
|
);
|
|
AssignedSets &bestPal = assignments[bestPalIndex];
|
|
// Add the color to that palette
|
|
bestPal.assign(std::move(attrs));
|
|
|
|
auto compareEfficiency = [&](ColorSetAttrs const &attrs1, ColorSetAttrs const &attrs2) {
|
|
ColorSet const &colorSet1 = colorSets[attrs1.colorSetIndex];
|
|
ColorSet const &colorSet2 = colorSets[attrs2.colorSetIndex];
|
|
size_t size1 = colorSet1.size();
|
|
size_t size2 = colorSet2.size();
|
|
uint32_t relSize1 = bestPal.relSizeOf(colorSet1);
|
|
uint32_t relSize2 = bestPal.relSizeOf(colorSet2);
|
|
|
|
verbosePrint(
|
|
VERB_TRACE,
|
|
" Color sets %zu <=> %zu: Efficiency: %zu / %" PRIu32 " <=> %zu / "
|
|
"%" PRIu32 "\n",
|
|
attrs1.colorSetIndex,
|
|
attrs2.colorSetIndex,
|
|
size1,
|
|
relSize1,
|
|
size2,
|
|
relSize2
|
|
);
|
|
|
|
// This comparison is algebraically equivalent to
|
|
// `size1 / relSize1 <=> size2 / relSize2`,
|
|
// but without potential precision loss from floating-point division.
|
|
size_t efficiency1 = size1 * relSize2;
|
|
size_t efficiency2 = size2 * relSize1;
|
|
return (efficiency1 > efficiency2) - (efficiency1 < efficiency2);
|
|
};
|
|
|
|
// If this overloads the palette, get it back to normal (if possible)
|
|
while (bestPal.volume() > options.maxOpaqueColors()) {
|
|
verbosePrint(
|
|
VERB_TRACE,
|
|
"Palette %zu is overloaded! (%zu > %" PRIu8 ")\n",
|
|
bestPalIndex,
|
|
bestPal.volume(),
|
|
options.maxOpaqueColors()
|
|
);
|
|
|
|
// Look for a color set minimizing "efficiency" (size / relSize)
|
|
auto [minEfficiencyIter, maxEfficiencyIter] = std::minmax_element(
|
|
RANGE(bestPal),
|
|
[&compareEfficiency](ColorSetAttrs const &lhs, ColorSetAttrs const &rhs) {
|
|
return compareEfficiency(lhs, rhs) < 0;
|
|
}
|
|
);
|
|
|
|
// All efficiencies are identical iff min equals max
|
|
if (compareEfficiency(*minEfficiencyIter, *maxEfficiencyIter) == 0) {
|
|
verbosePrint(VERB_TRACE, " All efficiencies are identical\n");
|
|
break;
|
|
}
|
|
|
|
// Remove the color set with minimal efficiency
|
|
verbosePrint(
|
|
VERB_TRACE, " Removing color set %zu\n", minEfficiencyIter->colorSetIndex
|
|
);
|
|
queue.emplace(std::move(*minEfficiencyIter));
|
|
queue.back().banFrom(bestPalIndex); // Ban it from this palette
|
|
bestPal.remove(minEfficiencyIter);
|
|
}
|
|
}
|
|
|
|
// Deal with palettes still overloaded, by emptying them
|
|
auto const &largestColorSetFirst =
|
|
[&colorSets](ColorSetAttrs const &lhs, ColorSetAttrs const &rhs) {
|
|
return colorSets[lhs.colorSetIndex].size() > colorSets[rhs.colorSetIndex].size();
|
|
};
|
|
std::vector<ColorSetAttrs> overloadQueue{};
|
|
for (AssignedSets &pal : assignments) {
|
|
if (pal.volume() > options.maxOpaqueColors()) {
|
|
for (ColorSetAttrs &attrs : pal) {
|
|
overloadQueue.emplace(
|
|
std::lower_bound(RANGE(overloadQueue), attrs, largestColorSetFirst),
|
|
std::move(attrs)
|
|
);
|
|
}
|
|
pal.clear();
|
|
}
|
|
}
|
|
// Place back any color sets now in the queue via first-fit
|
|
for (ColorSetAttrs const &attrs : overloadQueue) {
|
|
ColorSet const &colorSet = colorSets[attrs.colorSetIndex];
|
|
auto palette = std::find_if(RANGE(assignments), [&colorSet](AssignedSets const &pal) {
|
|
return pal.canFit(colorSet);
|
|
});
|
|
if (palette == assignments.end()) { // No such page, create a new one
|
|
verbosePrint(
|
|
VERB_DEBUG,
|
|
"Adding new palette (%zu) for overflowing color set %zu\n",
|
|
assignments.size(),
|
|
attrs.colorSetIndex
|
|
);
|
|
assignments.emplace_back(colorSets, std::move(attrs));
|
|
} else {
|
|
verbosePrint(
|
|
VERB_DEBUG,
|
|
"Assigning overflowing color set %zu to palette %zu\n",
|
|
attrs.colorSetIndex,
|
|
palette - assignments.begin()
|
|
);
|
|
palette->assign(std::move(attrs));
|
|
}
|
|
}
|
|
|
|
verboseOutputAssignments(assignments, colorSets); // LCOV_EXCL_LINE
|
|
|
|
// "Decant" the result
|
|
decant(assignments, colorSets);
|
|
// Note that the result does not contain any empty palettes
|
|
|
|
verboseOutputAssignments(assignments, colorSets); // LCOV_EXCL_LINE
|
|
|
|
std::vector<size_t> mappings(colorSets.size());
|
|
for (size_t i = 0; i < assignments.size(); ++i) {
|
|
for (ColorSetAttrs const &attrs : assignments[i]) {
|
|
mappings[attrs.colorSetIndex] = i;
|
|
}
|
|
}
|
|
return {mappings, assignments.size()};
|
|
}
|