diff --git a/doc/changelog.dox b/doc/changelog.dox index 4a1c99cf9..9a0899ad7 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -372,8 +372,7 @@ See also: @subsubsection changelog-latest-new-texturetools TextureTools library -- New @ref TextureTools::AtlasLandfill and - @relativeref{TextureTools,AtlasLandfillArray} texture atlas packers (see +- New @ref TextureTools::AtlasLandfill texture atlas packer (see [mosra/magnum#2](https://github.com/mosra/magnum/issues/2)) - New @ref TextureTools::atlasArrayPowerOfTwo() utility for optimal packing of power-of-two textures into a texture atlas array diff --git a/doc/generated/atlas.cpp b/doc/generated/atlas.cpp index 897df67cf..1e1b0f42b 100644 --- a/doc/generated/atlas.cpp +++ b/doc/generated/atlas.cpp @@ -137,7 +137,7 @@ int main() { Containers::BitArray rotations{NoInit, sizes.size()}; CORRADE_INTERNAL_ASSERT(atlas.add(sizes, offsets, rotations)); - Range2Di viewBox{{}, atlas.filledSize()}; + Range2Di viewBox{{}, atlas.filledSize().xy()}; std::string out; Utility::formatInto(out, out.size(), R"( diff --git a/doc/snippets/MagnumTextureTools.cpp b/doc/snippets/MagnumTextureTools.cpp index a9f8fedb2..240848835 100644 --- a/doc/snippets/MagnumTextureTools.cpp +++ b/doc/snippets/MagnumTextureTools.cpp @@ -52,7 +52,7 @@ TextureTools::AtlasLandfill atlas{{1024, 0}}; atlas.add(stridedArrayView(images).slice(&ImageView2D::size), offsets, rotations); /* Copy the image data to the atlas, assuming all are RGBA8Unorm as well */ -Image2D output{PixelFormat::RGBA8Unorm, atlas.filledSize(), +Image2D output{PixelFormat::RGBA8Unorm, atlas.filledSize().xy(), Containers::Array{ValueInit, std::size_t(atlas.filledSize().product())}}; Containers::StridedArrayView2D dst = output.pixels(); for(std::size_t i = 0; i != images.size(); ++i) { @@ -77,7 +77,7 @@ atlas.clearFlags(TextureTools::AtlasLandfillFlag::RotatePortrait| .add(stridedArrayView(images).slice(&ImageView2D::size), offsets); /* Copy the image data to the atlas, assuming all are RGBA8Unorm as well */ -Image2D output{PixelFormat::RGBA8Unorm, atlas.filledSize(), +Image2D output{PixelFormat::RGBA8Unorm, atlas.filledSize().xy(), Containers::Array{ValueInit, std::size_t(atlas.filledSize().product())}}; Containers::StridedArrayView2D dst = output.pixels(); for(std::size_t i = 0; i != images.size(); ++i) { @@ -90,13 +90,13 @@ for(std::size_t i = 0; i != images.size(); ++i) { } { -/* [AtlasLandfillArray-usage] */ +/* [AtlasLandfill-usage-array] */ Containers::ArrayView images = DOXYGEN_ELLIPSIS({}); Containers::Array offsets{NoInit, images.size()}; Containers::BitArray rotations{NoInit, images.size()}; /* Fill the atlas with an unbounded depth */ -TextureTools::AtlasLandfillArray atlas{{1024, 1024, 0}}; +TextureTools::AtlasLandfill atlas{{1024, 1024, 0}}; atlas.add(stridedArrayView(images).slice(&ImageView2D::size), offsets, rotations); /* Copy the image data to the atlas, assuming all are RGBA8Unorm as well */ @@ -114,7 +114,7 @@ for(std::size_t i = 0; i != images.size(); ++i) { std::size_t(offsets[i].y()), std::size_t(offsets[i].x())}, src.size())); } -/* [AtlasLandfillArray-usage] */ +/* [AtlasLandfill-usage-array] */ } { diff --git a/src/Magnum/TextureTools/Atlas.cpp b/src/Magnum/TextureTools/Atlas.cpp index 166e01a12..d38c49e40 100644 --- a/src/Magnum/TextureTools/Atlas.cpp +++ b/src/Magnum/TextureTools/Atlas.cpp @@ -187,16 +187,77 @@ bool atlasLandfillAddSortedFlipped(Implementation::AtlasLandfillState& state, co return true; } -bool atlasLandfillAdd(const char* messagePrefix, Implementation::AtlasLandfillState& state, const Containers::StridedArrayView1D sizes, const Containers::StridedArrayView1D offsets, const Containers::StridedArrayView1D zOffsets, const Containers::MutableBitArrayView rotations) { +} + +AtlasLandfill::AtlasLandfill(const Vector3i& size):_state{InPlaceInit} { + CORRADE_ASSERT(size.x(), "TextureTools::AtlasLandfill: expected non-zero width, got" << Debug::packed << size, ); + CORRADE_ASSERT(size.y() || size.z() == 1, "TextureTools::AtlasLandfill: expected a single array slice for unbounded height, got" << Debug::packed << size, ); + CORRADE_ASSERT(size.x() <= 65536, "TextureTools::AtlasLandfill: expected width to fit into 16 bits, got" << Debug::packed << size, ); + + /* Change y / z = 0 to y / z = MAX so the algorithm doesn't need to branch + on that internally */ + _state->size = {size.x(), + size.y() ? size.y() : 0x7fffffff, + size.z() ? size.z() : 0x7fffffff}; +} + +AtlasLandfill::AtlasLandfill(const Vector2i& size): AtlasLandfill{{size, 1}} {} + +AtlasLandfill::AtlasLandfill(AtlasLandfill&&) noexcept = default; + +AtlasLandfill::~AtlasLandfill() = default; + +AtlasLandfill& AtlasLandfill::operator=(AtlasLandfill&&) noexcept = default; + +Vector3i AtlasLandfill::size() const { + /* Change y / z = MAX (that's there so the algorithm doesn't need to branch + on that internally) back to y / z = 0 */ + return {_state->size.x(), + _state->size.y() == 0x7fffffff ? 0 : _state->size.y(), + _state->size.z() == 0x7fffffff ? 0 : _state->size.z()}; +} + +Vector3i AtlasLandfill::filledSize() const { + if(_state->size.z() == 1) + return {_state->size.x(), Math::max(_state->yOffsets), 1}; + + CORRADE_INTERNAL_ASSERT(_state->size.y()); + return {_state->size.xy(), Int(_state->slices.size())}; +} + +Vector2i AtlasLandfill::padding() const { + return _state->padding; +} + +AtlasLandfill& AtlasLandfill::setPadding(const Vector2i& padding) { + _state->padding = padding; + return *this; +} + +AtlasLandfillFlags AtlasLandfill::flags() const { + return _state->flags; +} + +AtlasLandfill& AtlasLandfill::setFlags(AtlasLandfillFlags flags) { + CORRADE_ASSERT(!(flags & AtlasLandfillFlag::RotatePortrait) || + !(flags & AtlasLandfillFlag::RotateLandscape), + "TextureTools::AtlasLandfill::setFlags(): only one of RotatePortrait and RotateLandscape can be set", *this); + CORRADE_ASSERT(!(flags & AtlasLandfillFlag::WidestFirst) || + !(flags & AtlasLandfillFlag::NarrowestFirst), + "TextureTools::AtlasLandfill::setFlags(): only one of WidestFirst and NarrowestFirst can be set", *this); + _state->flags = flags; + return *this; +} + +namespace { + +bool atlasLandfillAdd(Implementation::AtlasLandfillState& state, const Containers::StridedArrayView1D sizes, const Containers::StridedArrayView1D offsets, const Containers::StridedArrayView1D zOffsets, const Containers::MutableBitArrayView rotations) { CORRADE_ASSERT(offsets.size() == sizes.size(), - messagePrefix << "expected sizes and offsets views to have the same size, got" << sizes.size() << "and" << offsets.size(), {}); + "TextureTools::AtlasLandfill::add(): expected sizes and offsets views to have the same size, got" << sizes.size() << "and" << offsets.size(), {}); CORRADE_ASSERT((!(state.flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)) && rotations.isEmpty()) || rotations.size() == sizes.size(), - messagePrefix << "expected sizes and rotations views to have the same size, got" << sizes.size() << "and" << rotations.size(), {}); + "TextureTools::AtlasLandfill::add(): expected sizes and rotations views to have the same size, got" << sizes.size() << "and" << rotations.size(), {}); /* These are sliced internally from a Vector3i input, so should match */ CORRADE_INTERNAL_ASSERT(!zOffsets || zOffsets.size() == sizes.size()); - #ifdef CORRADE_NO_ASSERT - static_cast(messagePrefix); - #endif /* Nothing is flipped by default */ rotations.resetAll(); @@ -224,10 +285,10 @@ bool atlasLandfillAdd(const char* messagePrefix, Implementation::AtlasLandfillSt #ifndef CORRADE_NO_ASSERT if(state.padding.isZero()) CORRADE_ASSERT(size.product() && sizePadded <= state.size.xy(), - messagePrefix << "expected size" << i << "to be non-zero and not larger than" << Debug::packed << state.size.xy() << "but got" << Debug::packed << size, {}); + "TextureTools::AtlasLandfill::add(): expected size" << i << "to be non-zero and not larger than" << Debug::packed << state.size.xy() << "but got" << Debug::packed << size, {}); else CORRADE_ASSERT(size.product() && sizePadded <= state.size.xy(), - messagePrefix << "expected size" << i << "to be non-zero and not larger than" << Debug::packed << state.size.xy() << "but got" << Debug::packed << size << "and padding" << Debug::packed << padding, {}); + "TextureTools::AtlasLandfill::add(): expected size" << i << "to be non-zero and not larger than" << Debug::packed << state.size.xy() << "but got" << Debug::packed << size << "and padding" << Debug::packed << padding, {}); #endif sortedFlippedSizes[i] = {sizePadded, UnsignedInt(i)}; @@ -261,142 +322,41 @@ bool atlasLandfillAdd(const char* messagePrefix, Implementation::AtlasLandfillSt } -AtlasLandfill::AtlasLandfill(const Vector2i& size):_state{InPlaceInit} { - CORRADE_ASSERT(size.x(), "TextureTools::AtlasLandfill: expected non-zero width, got" << Debug::packed << size, ); - CORRADE_ASSERT(size.x() <= 65536, "TextureTools::AtlasLandfill: expected width to fit into 16 bits, got" << Debug::packed << size, ); - - /* Change y = 0 to y = MAX so the algorithm doesn't need to branch on that - internally */ - _state->size = {size.x(), - size.y() ? size.y() : 0x7fffffff, - 1}; -} - -AtlasLandfill::AtlasLandfill(AtlasLandfill&&) noexcept = default; - -AtlasLandfill::~AtlasLandfill() = default; - -AtlasLandfill& AtlasLandfill::operator=(AtlasLandfill&&) noexcept = default; - -Vector2i AtlasLandfill::size() const { - /* Change y = MAX (that's there so the algorithm doesn't need to branch on - that internally) back to y = 0 */ - return {_state->size.x(), - _state->size.y() == 0x7fffffff ? 0 : _state->size.y()}; -} - -Vector2i AtlasLandfill::filledSize() const { - return {_state->size.x(), Math::max(_state->yOffsets)}; +bool AtlasLandfill::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { + return atlasLandfillAdd(*_state, sizes, offsets.slice(&Vector3i::xy), offsets.slice(&Vector3i::z), flips); } -Vector2i AtlasLandfill::padding() const { - return _state->padding; -} - -AtlasLandfill& AtlasLandfill::setPadding(const Vector2i& padding) { - _state->padding = padding; - return *this; -} - -AtlasLandfillFlags AtlasLandfill::flags() const { - return _state->flags; -} - -AtlasLandfill& AtlasLandfill::setFlags(AtlasLandfillFlags flags) { - CORRADE_ASSERT(!(flags & AtlasLandfillFlag::RotatePortrait) || - !(flags & AtlasLandfillFlag::RotateLandscape), - "TextureTools::AtlasLandfill::setFlags(): only one of RotatePortrait and RotateLandscape can be set", *this); - CORRADE_ASSERT(!(flags & AtlasLandfillFlag::WidestFirst) || - !(flags & AtlasLandfillFlag::NarrowestFirst), - "TextureTools::AtlasLandfill::setFlags(): only one of WidestFirst and NarrowestFirst can be set", *this); - _state->flags = flags; - return *this; -} - -bool AtlasLandfill::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { - return atlasLandfillAdd("TextureTools::AtlasLandfill::add():", *_state, sizes, offsets, nullptr, flips); -} - -bool AtlasLandfill::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { +bool AtlasLandfill::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { return add(Containers::stridedArrayView(sizes), offsets, flips); } -bool AtlasLandfill::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets) { +bool AtlasLandfill::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets) { CORRADE_ASSERT(!(_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)), "TextureTools::AtlasLandfill::add():" << (_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)) << "set, expected a rotations view", {}); return add(sizes, offsets, nullptr); } -bool AtlasLandfill::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets) { +bool AtlasLandfill::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets) { return add(Containers::stridedArrayView(sizes), offsets); } -AtlasLandfillArray::AtlasLandfillArray(const Vector3i& size):_state{InPlaceInit} { - CORRADE_ASSERT(size.xy().product(), "TextureTools::AtlasLandfillArray: expected non-zero width and height, got" << Debug::packed << size, ); - CORRADE_ASSERT(size.x() <= 65536, "TextureTools::AtlasLandfillArray: expected width to fit into 16 bits, got" << Debug::packed << size, ); - - /* Change z = 0 to z = MAX so the algorithm doesn't need to branch on that - internally */ - _state->size = {size.xy(), - size.z() ? size.z() : 0x7fffffff}; -} - -AtlasLandfillArray::AtlasLandfillArray(AtlasLandfillArray&&) noexcept = default; - -AtlasLandfillArray::~AtlasLandfillArray() = default; - -AtlasLandfillArray& AtlasLandfillArray::operator=(AtlasLandfillArray&&) noexcept = default; - -Vector3i AtlasLandfillArray::size() const { - /* Change z = MAX (that's there so the algorithm doesn't need to branch on - that internally) back to z = 0 */ - return {_state->size.xy(), - _state->size.z() == 0x7fffffff ? 0 : _state->size.z()}; -} - -Vector3i AtlasLandfillArray::filledSize() const { - return {_state->size.xy(), Int(_state->slices.size())}; -} - -Vector2i AtlasLandfillArray::padding() const { - return _state->padding; -} - -AtlasLandfillArray& AtlasLandfillArray::setPadding(const Vector2i& padding) { - _state->padding = padding; - return *this; -} - -AtlasLandfillFlags AtlasLandfillArray::flags() const { - return _state->flags; -} - -AtlasLandfillArray& AtlasLandfillArray::setFlags(AtlasLandfillFlags flags) { - CORRADE_ASSERT(!(flags & AtlasLandfillFlag::RotatePortrait) || - !(flags & AtlasLandfillFlag::RotateLandscape), - "TextureTools::AtlasLandfillArray::setFlags(): only one of RotatePortrait and RotateLandscape can be set", *this); - CORRADE_ASSERT(!(flags & AtlasLandfillFlag::WidestFirst) || - !(flags & AtlasLandfillFlag::NarrowestFirst), - "TextureTools::AtlasLandfillArray::setFlags(): only one of WidestFirst and NarrowestFirst can be set", *this); - _state->flags = flags; - return *this; -} - -bool AtlasLandfillArray::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { - return atlasLandfillAdd("TextureTools::AtlasLandfillArray::add():", *_state, sizes, offsets.slice(&Vector3i::xy), offsets.slice(&Vector3i::z), flips); +bool AtlasLandfill::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { + CORRADE_ASSERT(_state->size.z() == 1, + "TextureTools::AtlasLandfill::add(): use the three-component overload for an array atlas", {}); + return atlasLandfillAdd(*_state, sizes, offsets, nullptr, flips); } -bool AtlasLandfillArray::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { +bool AtlasLandfill::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView flips) { return add(Containers::stridedArrayView(sizes), offsets, flips); } -bool AtlasLandfillArray::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets) { +bool AtlasLandfill::add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets) { CORRADE_ASSERT(!(_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)), - "TextureTools::AtlasLandfillArray::add():" << (_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)) << "set, expected a rotations view", {}); + "TextureTools::AtlasLandfill::add():" << (_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)) << "set, expected a rotations view", {}); return add(sizes, offsets, nullptr); } -bool AtlasLandfillArray::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets) { +bool AtlasLandfill::add(const std::initializer_list sizes, const Containers::StridedArrayView1D& offsets) { return add(Containers::stridedArrayView(sizes), offsets); } diff --git a/src/Magnum/TextureTools/Atlas.h b/src/Magnum/TextureTools/Atlas.h index 6ca3bde53..320aec680 100644 --- a/src/Magnum/TextureTools/Atlas.h +++ b/src/Magnum/TextureTools/Atlas.h @@ -26,7 +26,7 @@ */ /** @file - * @brief Class @ref Magnum::TextureTools::AtlasLandfill, @ref Magnum::TextureTools::AtlasLandfillArray, enum @ref Magnum::TextureTools::AtlasLandfillFlag, enum set @ref Magnum::TextureTools::AtlasLandfillFlags, function @ref Magnum::TextureTools::atlas(), @ref Magnum::TextureTools::atlasArrayPowerOfTwo() + * @brief Class @ref Magnum::TextureTools::AtlasLandfill, enum @ref Magnum::TextureTools::AtlasLandfillFlag, enum set @ref Magnum::TextureTools::AtlasLandfillFlags, function @ref Magnum::TextureTools::atlas(), @ref Magnum::TextureTools::atlasArrayPowerOfTwo() */ #include @@ -51,9 +51,7 @@ namespace Implementation { @m_since_latest @see @ref AtlasLandfillFlags, @ref AtlasLandfill::setFlags(), - @ref AtlasLandfill::addFlags(), @ref AtlasLandfill::clearFlags(), - @ref AtlasLandfillArray::setFlags(), @ref AtlasLandfillArray::addFlags(), - @ref AtlasLandfillArray::clearFlags() + @ref AtlasLandfill::addFlags(), @ref AtlasLandfill::clearFlags() */ enum class AtlasLandfillFlag { /** @@ -106,8 +104,7 @@ MAGNUM_TEXTURETOOLS_EXPORT Debug& operator<<(Debug& output, AtlasLandfillFlag va @m_since_latest @see @ref Flags, @ref AtlasLandfill::setFlags(), @ref AtlasLandfill::addFlags(), - @ref AtlasLandfill::clearFlags(), @ref AtlasLandfillArray::setFlags(), - @ref AtlasLandfillArray::addFlags(), @ref AtlasLandfillArray::clearFlags() + @ref AtlasLandfill::clearFlags() */ typedef Containers::EnumSet AtlasLandfillFlags; @@ -121,10 +118,10 @@ MAGNUM_TEXTURETOOLS_EXPORT Debug& operator<<(Debug& output, AtlasLandfillFlags v @m_since_latest Keeps track of currently filled height at every pixel with the aim to fill the -available space bottom-up as evenly as possible. Packs to a 2D texture with the -height optionally unbounded. See @ref AtlasLandfillArray for a variant that -works with 2D texture arrays, and @ref atlasArrayPowerOfTwo() for a variant -that always provides optimal packing for power-of-two sizes. +available space bottom-up as evenly as possible. Packs to a 2D or a 2D array +texture with either the height or depth optionally unbounded. See also +@ref atlasArrayPowerOfTwo() for a variant that always provides optimal packing +for power-of-two sizes. @htmlinclude atlas-landfill.svg @@ -147,6 +144,14 @@ overload without the rotations argument. @snippet MagnumTextureTools.cpp AtlasLandfill-usage-no-rotation +@subsection TextureTools-AtlasLandfill-usage-atlas Array atlas + +The packing can be extended to a third dimension as well, in which case the +packing overflows to next slices instead of expanding to potentially unbounded +height. + +@snippet MagnumTextureTools.cpp AtlasLandfill-usage-array + @section TextureTools-AtlasLandfill-process Packing process On every @ref add(), the algorithm first makes all sizes the same orientation @@ -170,9 +175,9 @@ bounded and the next item cannot fit there anymore. The sort is performed using @ref std::stable_sort(), which is usually @f$ \mathcal{O}(n \log{} n) @f$, the actual atlasing is a single @f$ \mathcal{O}(n) @f$ operation. Memory complexity is -@f$ \mathcal{O}(n + w) @f$ with @f$ n @f$ being a sorted copy of the input size -array and @f$ w @f$ being a 16-bit integer for every pixel of atlas width, -additionally @ref std::stable_sort() performs its own allocation. +@f$ \mathcal{O}(n + wc) @f$ with @f$ n @f$ being a sorted copy of the input +size array and @f$ wc @f$ being a 16-bit integer for every pixel of atlas width +times filled atlas depth. Additionally @ref std::stable_sort() performs its own allocation. @section TextureTools-AtlasLandfill-incremental Incremental population @@ -182,6 +187,12 @@ ideal scenario, if the previous fill resulted in an uniform height the newly added data will be added in an optimal way as well, but in practice calling @ref add() with all data just once will always result in a more optimal packing than an incremental one. + +In case of an array atlas, the incremental process always starts from the first +slice, finding the first that can fit the first (sorted) item. Then it attempts +to place as many items as possible and on overflow continues searching for the +next slice that can fit the first remaining item. If all slices are exhausted, +adds a new one for as long as the depth (if bounded) allows. */ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { public: @@ -189,8 +200,17 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { * @brief Constructor * * The @p size is expected to have non-zero width, and height not - * larger than 65536. If height is zero, the dimension is treated as - * unbounded, i.e. @ref add() never fails. + * larger than 65536. If height is @cpp 0 @ce, depth is expected to be + * @cpp 1 @ce and the height is treated as unbounded, i.e. @ref add() + * never fails. Otherwise, if depth is @cpp 0 @ce, depth is treated as + * unbounded. + */ + explicit AtlasLandfill(const Vector3i& size); + + /** + * @brief Construct a non-array atlas + * + * Same as calling @ref AtlasLandfill with depth set to @cpp 1 @ce. */ explicit AtlasLandfill(const Vector2i& size); @@ -213,17 +233,24 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { * * @see @ref filledSize() */ - Vector2i size() const; + Vector3i size() const; /** * @brief Currently filled size * - * Width is always taken from @ref size(). The height is @cpp 0 @ce - * initially, and at most the height of @ref size() if it's bounded. - * The size is calculated with a @f$ \mathcal{O}(w) @f$ complexity, - * with @f$ w @f$ being the atlas width. + * Width is always taken from @ref size(). + * + * If @ref size() depth is @cpp 1 @ce, the returned depth is always + * @cpp 1 @ce, height is @cpp 0 @ce initially, and at most the + * height of @ref size() if it's bounded. It's calculated with a + * @f$ \mathcal{O}(w) @f$ complexity, with @f$ w @f$ being the atlas + * width. + * + * Otherwise, if @ref size() depth is not @cpp 1 @ce, the height is + * taken from @ref size() and the depth is @cpp 0 @ce initially, and + * at most @ref size() depth if the size is bounded. */ - Vector2i filledSize() const; + Vector3i filledSize() const; /** * @brief Behavior flags @@ -235,15 +262,18 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { /** * @brief Set behavior flags + * @return Reference to self (for method chaining) * - * Can be called with different values before each particular - * @ref add(). + * Note that some flags are mutually exclusive, see documentation of + * particular @ref AtlasLandfillFlag values for more information. Can + * be called with different values before each particular @ref add(). * @see @ref addFlags(), @ref clearFlags() */ AtlasLandfill& setFlags(AtlasLandfillFlags flags); /** * @brief Add behavior flags + * @return Reference to self (for method chaining) * * Calls @ref setFlags() with the existing flags ORed with @p flags. * Useful for preserving the defaults. @@ -255,6 +285,7 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { /** * @brief Clear behavior flags + * @return Reference to self (for method chaining) * * Calls @ref setFlags() with the existing flags ANDed with the inverse * of @p flags. Useful for preserving the defaults. @@ -273,9 +304,11 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { /** * @brief Set padding around each texture + * @return Reference to self (for method chaining) * * Sizes are extended with twice the padding value before placement but - * the returned offsets are without padding again. In order to have + * the returned offsets are without padding again. The third dimension + * isn't treated in any special way. In order to have * @ref AtlasLandfillFlag::RotatePortrait and * @relativeref{AtlasLandfillFlag,RotateLandscape} work well also * with non-uniform padding, the padding is applied *before* a @@ -305,8 +338,8 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { * @relativeref{AtlasLandfillFlag,RotateLandscape} is set, the * @p rotations view can be also empty or you can use the * @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) - * overload. The @p offsets always point to the original potentially - * rotated sizes without padding applied. + * overload. The resulting @p offsets always point to the original + * (potentially rotated) sizes without padding applied. * * On success returns @cpp true @ce and updates @ref filledSize(). If * @ref size() is bounded, can return @cpp false @ce if the items @@ -315,208 +348,49 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { * @ref size() returns @cpp true @ce always. * @see @ref setFlags(), @ref setPadding() */ - bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); + bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); /** @overload */ - bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); + bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); /** * @brief Add textures to the atlas with rotations disabled * - * Equivalent to calling @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, Containers::MutableBitArrayView) + * Equivalent to calling @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, Containers::MutableBitArrayView) * with the @p rotations view being empty. Can be called only if * neither @ref AtlasLandfillFlag::RotatePortrait nor * @relativeref{AtlasLandfillFlag,RotateLandscape} is set. * @see @ref clearFlags() */ - bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets); + bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets); /** @overload */ - bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets); - - private: - Containers::Pointer _state; -}; - -/** -@brief Landfill texture atlas packer -@m_since_latest - -Extends @ref AtlasLandfill to a third dimension. Instead of expanding to an -unbounded height, on overflow a new texture slice is made. See also -@ref atlasArrayPowerOfTwo() for a variant that always provides optimal packing -for power-of-two sizes. - -@section TextureTools-AtlasLandfillArray-usage Example usage - -Compared to the @ref TextureTools-AtlasLandfill-usage "2D usage" it's extended -to three dimensions: - -@snippet MagnumTextureTools.cpp AtlasLandfillArray-usage - -@section TextureTools-AtlasLandfillArray-process Packing process - -Apart from expanding to new slices on height overflow, the underlying process -is @ref TextureTools-AtlasLandfill-process "the same as in AtlasLandfill". - -In this case, memory complexity is @f$ \mathcal{O}(n + wd) @f$ with @f$ n @f$ -being a sorted copy of the input size array and @f$ wd @f$ being a 16-bit -integer for every pixel of atlas width times atlas depth. - -@section TextureTools-AtlasLandfillArray-incremental Incremental population - -Compared to the @ref TextureTools-AtlasLandfill-incremental "2D incremental population", -the incremental process always starts from the first slice, finding the first -that can fit the first (sorted) item. Then it attempts to place as many items -as possible and on overflow continues searching for the next slice that can fit -the first remaining item. If all slices are exhausted, adds a new one. -*/ -class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfillArray { - public: - /** - * @brief Constructor - * - * The @p size has to have non-zero width and height. If depth is - * @cpp 0 @ce, the dimension is treated as unbounded, i.e. @ref add() - * never fails. If depth is @cpp 1 @ce, behaves the same as - * @ref AtlasLandfillArray with a bounded height. - */ - explicit AtlasLandfillArray(const Vector3i& size); - - /** @brief Copying is not allowed */ - AtlasLandfillArray(const AtlasLandfillArray&) = delete; - - /** @brief Move constructor */ - AtlasLandfillArray(AtlasLandfillArray&&) noexcept; - - ~AtlasLandfillArray(); - - /** @brief Copying is not allowed */ - AtlasLandfillArray& operator=(const AtlasLandfillArray&) = delete; - - /** @brief Move assignment */ - AtlasLandfillArray& operator=(AtlasLandfillArray&&) noexcept; - - /** - * @brief Atlas size specified in constructor - * - * @see @ref filledSize() - */ - Vector3i size() const; - - /** - * @brief Currently filled size - * - * Width and height is always taken from @ref size(). The depth is - * @cpp 0 @ce initially, and at most @ref size() depth if the size is - * bounded. - */ - Vector3i filledSize() const; - - /** @brief Behavior flags */ - AtlasLandfillFlags flags() const; - - /** - * @brief Set behavior flags - * - * Can be called with different values before each particular - * @ref add(). Default is @ref AtlasLandfillFlag::RotatePortrait. - * @see @ref addFlags(), @ref clearFlags() - */ - AtlasLandfillArray& setFlags(AtlasLandfillFlags flags); - - /** - * @brief Add behavior flags - * - * Calls @ref setFlags() with the existing flags ORed with @p flags. - * Useful for preserving the defaults. - * @see @ref clearFlags() - */ - AtlasLandfillArray& addFlags(AtlasLandfillFlags flags) { - return setFlags(this->flags()|flags); - } - - /** - * @brief Clear behavior flags - * - * Calls @ref setFlags() with the existing flags ANDed with the inverse - * of @p flags. Useful for preserving the defaults. - * @see @ref addFlags() - */ - AtlasLandfillArray& clearFlags(AtlasLandfillFlags flags) { - return setFlags(this->flags() & ~flags); - } - - /** - * @brief Padding around each texture - * - * Default is a zero vector. - */ - Vector2i padding() const; - - /** - * @brief Set padding around each texture - * - * Sizes are extended with twice the padding value before placement but - * the returned offsets are without padding again. The third dimension - * isn't treated in any special way. In order to have - * @ref AtlasLandfillFlag::RotatePortrait and - * @relativeref{AtlasLandfillFlag,RotateLandscape} work well also - * with non-uniform padding, the padding is applied *before* a - * potential rotation. I.e., the horizontal padding value is always - * applied on input image width independently on how it's rotated - * after. If you need different behavior, disable rotations with - * @ref clearFlags() or pre-pad the input sizes directly instead of - * using this function. - * - * Can be called with different values before each particular - * @ref add(). - */ - AtlasLandfillArray& setPadding(const Vector2i& padding); + bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets); /** - * @brief Add textures to the atlas - * @param[in] sizes Texture sizes - * @param[out] offsets Resulting offsets in the atlas - * @param[out] rotations Which textures got rotated - * - * The @p sizes, @p offsets and @p rotations views are expected to have - * the same size. The @p sizes are all expected to be non-zero and not - * larger than @ref size() after applying padding and then a rotation - * based on @ref AtlasLandfillFlag::RotatePortrait or - * @relativeref{AtlasLandfillFlag,RotateLandscape} being set. If - * neither @relativeref{AtlasLandfillFlag,RotatePortrait} nor - * @relativeref{AtlasLandfillFlag,RotateLandscape} is set, the - * @p rotations view can be also empty or you can use the - * @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) - * overload. The @p offsets always point to the original potentially - * rotated sizes without padding applied. + * @brief Add textures to a non-array atlas * - * On success returns @cpp true @ce and updates @ref filledSize(). If - * @ref size() is bounded, can return @cpp false @ce if the items - * didn't fit, in which case the internals and contents of @p offsets - * and @p rotations are left in an undefined state. For an unbounded - * @ref size() returns @cpp true @ce always. - * @see @ref setFlags(), @ref setPadding() + * Can be called only if @ref size() depth is @cpp 1 @ce. */ - bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); + bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); /** @overload */ - bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); + bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); /** - * @brief Add textures to the atlas with rotations disabled + * @brief Add textures to a non-array atlas with rotations disabled * - * Equivalent to calling @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, Containers::MutableBitArrayView) + * Equivalent to calling @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&, Containers::MutableBitArrayView) * with the @p rotations view being empty. Can be called only if - * neither @ref AtlasLandfillFlag::RotatePortrait nor + * @ref size() depth is @cpp 1 @ce and neither + * @ref AtlasLandfillFlag::RotatePortrait nor * @relativeref{AtlasLandfillFlag,RotateLandscape} is set. * @see @ref clearFlags() */ - bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets); + bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets); /** @overload */ - bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets); + bool add(std::initializer_list sizes, const Containers::StridedArrayView1D& offsets); private: Containers::Pointer _state; @@ -572,9 +446,9 @@ array, additionally @ref std::stable_sort() performs its own allocation. See the [Zero-waste single-pass packing of power-of-two textures](https://blog.magnum.graphics/backstage/pot-array-packing/) article for a detailed description of the algorithm. -See the @ref AtlasLandfill and @ref AtlasLandfillArray classes for an -alternative that isn't restricted to power-of-two sizes and can be used in an -incremental way but doesn't always produce optimal packing. +See the @ref AtlasLandfill class for an alternative that isn't restricted to +power-of-two sizes and can be used in an incremental way but doesn't always +produce optimal packing. */ MAGNUM_TEXTURETOOLS_EXPORT Int atlasArrayPowerOfTwo(const Vector2i& layerSize, const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets); diff --git a/src/Magnum/TextureTools/Test/AtlasBenchmark.cpp b/src/Magnum/TextureTools/Test/AtlasBenchmark.cpp index ada82015e..d11632409 100644 --- a/src/Magnum/TextureTools/Test/AtlasBenchmark.cpp +++ b/src/Magnum/TextureTools/Test/AtlasBenchmark.cpp @@ -381,7 +381,7 @@ void AtlasBenchmark::landfill() { CORRADE_COMPARE_WITH( Containers::pair(Containers::StridedArrayView1D{offsets}, Containers::BitArrayView{flips}), _sizes, - (CompareAtlasPacking{data.image, atlas.filledSize()})); + (CompareAtlasPacking{data.image, atlas.filledSize().xy()})); } void AtlasBenchmark::stbRectPack() { diff --git a/src/Magnum/TextureTools/Test/AtlasTest.cpp b/src/Magnum/TextureTools/Test/AtlasTest.cpp index 395a93378..bfe6870b6 100644 --- a/src/Magnum/TextureTools/Test/AtlasTest.cpp +++ b/src/Magnum/TextureTools/Test/AtlasTest.cpp @@ -65,13 +65,12 @@ struct AtlasTest: TestSuite::Tester { void landfillArrayIncremental(); void landfillArrayPadded(); void landfillArrayNoFit(); - void landfillArrayCopy(); - void landfillArrayMove(); void landfillInvalidSize(); void landfillSetFlagsInvalid(); void landfillAddMissingRotations(); void landfillAddInvalidViewSizes(); + void landfillAddTwoComponentForArray(); void landfillAddTooLargeElement(); void landfillAddTooLargeElementPadded(); @@ -114,14 +113,14 @@ const Vector2i LandfillSizes[]{ const struct { const char* name; AtlasLandfillFlags flags; - Vector2i size; - Vector2i filledSize; + Vector3i size; + Vector3i filledSize; Containers::Pair offsetsFlips[Containers::arraySize(LandfillSizes)]; } LandfillData[]{ /* In all of these, rectangles with the same size should keep their order. 5 after 3, 9 after 8 after 6 (and b after a after 7 if they're rotated to the same orientation) */ - {"no rotation, no width sorting", {}, {11, 12}, {11, 9}, { + {"no rotation, no width sorting", {}, {11, 12, 1}, {11, 9, 1}, { /* Here it discovers that item 8 is higher than 5 and so it begins from the opposite end in the same direction again, instead of flipping the direction at item 8. @@ -149,7 +148,7 @@ const struct { {{8, 6}, false}, /* b */ {{3, 8}, false}}}, /* c */ /* No rotation with width sorting omitted, not interesting */ - {"portrait, no width sorting", AtlasLandfillFlag::RotatePortrait, {11, 12}, {11, 9}, { + {"portrait, no width sorting", AtlasLandfillFlag::RotatePortrait, {11, 12, 1}, {11, 9, 1}, { /* Here it should compare against the height of item 8, not item 0. Which is again higher than item 4 on the other side so it again begins from the opposite side. @@ -176,7 +175,7 @@ const struct { {{8, 7}, true}, /* a */ {{7, 7}, false}, /* b */ {{6, 7}, false}}}, /* c */ - {"portrait, widest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 12}, {11, 8}, { + {"portrait, widest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 12, 1}, {11, 8, 1}, { /* 9988 cba7 99886644ba7 000 6644555 @@ -198,7 +197,7 @@ const struct { {{9, 6}, true}, /* a */ {{8, 6}, false}, /* b */ {{7, 7}, false}}}, /* c */ - {"portrait, widest first, unbounded height", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 0}, {11, 8}, { + {"portrait, widest first, unbounded height", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 0, 1}, {11, 8, 1}, { /* Should have the same result as above. * 9988 cba7 @@ -222,7 +221,7 @@ const struct { {{9, 6}, true}, /* a */ {{8, 6}, false}, /* b */ {{7, 7}, false}}}, /* c */ - {"portrait, widest first, reverse direction always", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst|AtlasLandfillFlag::ReverseDirectionAlways, {11, 12}, {11, 10}, { + {"portrait, widest first, reverse direction always", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst|AtlasLandfillFlag::ReverseDirectionAlways, {11, 12, 1}, {11, 10, 1}, { /* Here it continues in reverse direction after placing item 9 even though it's higher than item 5 as it's forced to. @@ -249,7 +248,7 @@ const struct { {{1, 8}, true}, /* a */ {{2, 8}, false}, /* b */ {{3, 8}, false}}}, /* c */ - {"portrait, narrowest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::NarrowestFirst, {11, 12}, {11, 9}, { + {"portrait, narrowest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::NarrowestFirst, {11, 12, 1}, {11, 9, 1}, { /* 99 66b c9988 66ba7555 88 @@ -272,7 +271,7 @@ const struct { {{3, 5}, true}, /* a */ {{2, 6}, false}, /* b */ {{6, 7}, false}}}, /* c */ - {"landscape, no width sorting", AtlasLandfillFlag::RotateLandscape, {11, 12}, {11, 9}, { + {"landscape, no width sorting", AtlasLandfillFlag::RotateLandscape, {11, 12, 1}, {11, 9, 1}, { /* After placing 3 it continues in reverse direction as 0 isn't lower (i.e., same behavior as if reversal was forced, and makes sense); after placing 1 it continues in reverse direction with 2 again; @@ -300,7 +299,7 @@ const struct { {{4, 7}, false}, /* a */ {{6, 8}, true}, /* b */ {{8, 8}, false}}}, /* c */ - {"landscape, widest first", AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::WidestFirst, {11, 12}, {11, 9}, { + {"landscape, widest first", AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::WidestFirst, {11, 12, 1}, {11, 9, 1}, { /* No change compared to "no width sorting" in this case. 99 bbc @@ -325,7 +324,7 @@ const struct { {{4, 7}, false}, /* a */ {{6, 8}, true}, /* b */ {{8, 8}, false}}}, /* c */ - {"landscape, narrowest first", AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::NarrowestFirst, {11, 12}, {11, 10}, { + {"landscape, narrowest first", AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::NarrowestFirst, {11, 12, 1}, {11, 10, 1}, { /* No special behavior worth commenting on here. Flips direction after placing 5, after 8, and doesn't after placing 2. @@ -489,13 +488,12 @@ AtlasTest::AtlasTest() { addTests({&AtlasTest::landfillArrayIncremental, &AtlasTest::landfillArrayPadded, &AtlasTest::landfillArrayNoFit, - &AtlasTest::landfillArrayCopy, - &AtlasTest::landfillArrayMove, &AtlasTest::landfillInvalidSize, &AtlasTest::landfillSetFlagsInvalid, &AtlasTest::landfillAddMissingRotations, &AtlasTest::landfillAddInvalidViewSizes, + &AtlasTest::landfillAddTwoComponentForArray, &AtlasTest::landfillAddTooLargeElement, &AtlasTest::landfillAddTooLargeElementPadded, @@ -544,8 +542,8 @@ void AtlasTest::landfillFullFit() { a tight fit */ AtlasLandfill atlas{{4, 6}}; - CORRADE_COMPARE(atlas.size(), (Vector2i{4, 6})); - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{4, 0})); + CORRADE_COMPARE(atlas.size(), (Vector3i{4, 6, 1})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{4, 0, 1})); CORRADE_COMPARE(atlas.flags(), AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst); CORRADE_COMPARE(atlas.padding(), Vector2i{}); @@ -559,7 +557,7 @@ void AtlasTest::landfillFullFit() { {2, 3}, /* 2 */ {2, 2}, /* 3 */ }, offsets, rotations)); - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{4, 6})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{4, 6, 1})); CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({ false, false, false, false }).sliceBit(0), TestSuite::Compare::Container); @@ -639,25 +637,25 @@ void AtlasTest::landfillIncremental() { Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(sizeData)}; AtlasLandfill atlas{{11, 8}}; - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 0})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 0, 1})); CORRADE_VERIFY(atlas.add( sizes.prefix(5), offsets.prefix(5), rotations.prefix(5))); - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 6})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 6, 1})); CORRADE_VERIFY(atlas.add( sizes.slice(5, 9), offsets.slice(5, 9), rotations.slice(5, 9))); - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 8})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 8, 1})); CORRADE_VERIFY(atlas.add( sizes.exceptPrefix(9), offsets.exceptPrefix(9), rotations.exceptPrefix(9))); - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 8})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 8, 1})); CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({ true, false, false, true, false, false, false, false, true, false, @@ -706,7 +704,7 @@ void AtlasTest::landfillPadded() { {1, 1}, /* 5, padded to {3, 5} */ }, offsets, rotations)); - CORRADE_COMPARE(atlas.filledSize(), (Vector2i{15, 13})); + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{15, 13, 1})); CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({ true, false, true, false, false, false }).sliceBit(0), TestSuite::Compare::Container); @@ -754,20 +752,20 @@ void AtlasTest::landfillCopy() { } void AtlasTest::landfillMove() { - AtlasLandfill a{{16, 24}}; + AtlasLandfill a{{16, 24, 8}}; - Vector2i offsets[2]; + Vector3i offsets[2]; UnsignedByte rotations[1]; - CORRADE_VERIFY(a.add({{15, 17}, {2, 3}}, offsets, Containers::MutableBitArrayView{rotations, 0, 2})); + CORRADE_VERIFY(a.add({{12, 17}, {5, 12}}, offsets, Containers::MutableBitArrayView{rotations, 0, 2})); AtlasLandfill b = Utility::move(a); - CORRADE_COMPARE(b.size(), (Vector2i{16, 24})); - CORRADE_COMPARE(b.filledSize(), (Vector2i{16, 20})); + CORRADE_COMPARE(b.size(), (Vector3i{16, 24, 8})); + CORRADE_COMPARE(b.filledSize(), (Vector3i{16, 24, 2})); - AtlasLandfill c{{16, 12}}; + AtlasLandfill c{{16, 12, 1}}; c = Utility::move(b); - CORRADE_COMPARE(c.size(), (Vector2i{16, 24})); - CORRADE_COMPARE(c.filledSize(), (Vector2i{16, 20})); + CORRADE_COMPARE(c.size(), (Vector3i{16, 24, 8})); + CORRADE_COMPARE(c.filledSize(), (Vector3i{16, 24, 2})); CORRADE_VERIFY(std::is_nothrow_move_constructible::value); CORRADE_VERIFY(std::is_nothrow_move_assignable::value); @@ -777,7 +775,7 @@ void AtlasTest::landfillArrayFullFit() { /* Trivial case to verify there are no off-by-one errors that would prevent a tight fit */ - AtlasLandfillArray atlas{{4, 5, 2}}; + AtlasLandfill atlas{{4, 5, 2}}; CORRADE_COMPARE(atlas.size(), (Vector3i{4, 5, 2})); CORRADE_COMPARE(atlas.filledSize(), (Vector3i{4, 5, 0})); CORRADE_COMPARE(atlas.flags(), AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst); @@ -819,7 +817,7 @@ void AtlasTest::landfillArray() { auto&& data = LandfillArrayData[testCaseInstanceId()]; setTestCaseDescription(data.name); - AtlasLandfillArray atlas{data.size}; + AtlasLandfill atlas{data.size}; /* For unbounded sizes it should return 0 again */ CORRADE_COMPARE(atlas.size(), data.size); @@ -871,7 +869,7 @@ void AtlasTest::landfillArrayIncremental() { UnsignedByte rotationData[2]; Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(sizeData)}; - AtlasLandfillArray atlas{{11, 6, 2}}; + AtlasLandfill atlas{{11, 6, 2}}; CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 6, 0})); CORRADE_VERIFY(atlas.add( @@ -917,9 +915,9 @@ void AtlasTest::landfillArrayIncremental() { } void AtlasTest::landfillArrayPadded() { - /* Like landfillPadded(), but item 5 overlflowing to the next slice */ + /* Like landfillPadded(), but item 5 overflowing to the next slice */ - AtlasLandfillArray atlas{{15, 12, 3}}; + AtlasLandfill atlas{{15, 12, 3}}; atlas.setPadding({1, 2}); CORRADE_COMPARE(atlas.padding(), (Vector2i{1, 2})); @@ -968,7 +966,7 @@ void AtlasTest::landfillArrayNoFit() { /* Same as landfillArray(portrait, widest first) (which is the default flags) which fits into {11, 6, 2} but limiting depth to 1 */ - AtlasLandfillArray atlas{{11, 6, 1}}; + AtlasLandfill atlas{{11, 6, 1}}; Vector3i offsets[Containers::arraySize(LandfillArraySizes)]; UnsignedByte rotationData[2]; @@ -976,53 +974,28 @@ void AtlasTest::landfillArrayNoFit() { CORRADE_VERIFY(!atlas.add(LandfillArraySizes, offsets, rotations)); } -void AtlasTest::landfillArrayCopy() { - CORRADE_VERIFY(!std::is_copy_constructible{}); - CORRADE_VERIFY(!std::is_copy_assignable{}); -} - -void AtlasTest::landfillArrayMove() { - AtlasLandfillArray a{{16, 24, 8}}; - - Vector3i offsets[2]; - UnsignedByte rotations[1]; - CORRADE_VERIFY(a.add({{12, 17}, {5, 12}}, offsets, Containers::MutableBitArrayView{rotations, 0, 2})); - - AtlasLandfillArray b = Utility::move(a); - CORRADE_COMPARE(b.size(), (Vector3i{16, 24, 8})); - CORRADE_COMPARE(b.filledSize(), (Vector3i{16, 24, 2})); - - AtlasLandfillArray c{{16, 12, 1}}; - c = Utility::move(b); - CORRADE_COMPARE(c.size(), (Vector3i{16, 24, 8})); - CORRADE_COMPARE(c.filledSize(), (Vector3i{16, 24, 2})); - - CORRADE_VERIFY(std::is_nothrow_move_constructible::value); - CORRADE_VERIFY(std::is_nothrow_move_assignable::value); -} - void AtlasTest::landfillInvalidSize() { CORRADE_SKIP_IF_NO_ASSERT(); /* These are fine */ AtlasLandfill{{16, 0}}; AtlasLandfill{{65536, 16}}; - AtlasLandfillArray{{16, 16, 0}}; - AtlasLandfillArray{{65536, 16, 16}}; + AtlasLandfill{{16, 16, 0}}; + AtlasLandfill{{65536, 16, 16}}; std::ostringstream out; Error redirectError{&out}; AtlasLandfill{{0, 16}}; AtlasLandfill{{65537, 16}}; - AtlasLandfillArray{{0, 16, 16}}; - AtlasLandfillArray{{16, 0, 16}}; - AtlasLandfillArray{{65537, 16, 16}}; + AtlasLandfill{{0, 16, 16}}; + AtlasLandfill{{16, 0, 16}}; + AtlasLandfill{{65537, 16, 16}}; CORRADE_COMPARE_AS(out.str(), - "TextureTools::AtlasLandfill: expected non-zero width, got {0, 16}\n" - "TextureTools::AtlasLandfill: expected width to fit into 16 bits, got {65537, 16}\n" - "TextureTools::AtlasLandfillArray: expected non-zero width and height, got {0, 16, 16}\n" - "TextureTools::AtlasLandfillArray: expected non-zero width and height, got {16, 0, 16}\n" - "TextureTools::AtlasLandfillArray: expected width to fit into 16 bits, got {65537, 16, 16}\n", + "TextureTools::AtlasLandfill: expected non-zero width, got {0, 16, 1}\n" + "TextureTools::AtlasLandfill: expected width to fit into 16 bits, got {65537, 16, 1}\n" + "TextureTools::AtlasLandfill: expected non-zero width, got {0, 16, 16}\n" + "TextureTools::AtlasLandfill: expected a single array slice for unbounded height, got {16, 0, 16}\n" + "TextureTools::AtlasLandfill: expected width to fit into 16 bits, got {65537, 16, 16}\n", TestSuite::Compare::String); } @@ -1030,49 +1003,40 @@ void AtlasTest::landfillSetFlagsInvalid() { CORRADE_SKIP_IF_NO_ASSERT(); AtlasLandfill atlas{{16, 16}}; - AtlasLandfillArray array{{16, 16, 1}}; std::ostringstream out; Error redirectError{&out}; atlas.setFlags(AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape); - array.setFlags(AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape); atlas.setFlags(AtlasLandfillFlag::WidestFirst|AtlasLandfillFlag::NarrowestFirst); - array.setFlags(AtlasLandfillFlag::WidestFirst|AtlasLandfillFlag::NarrowestFirst); CORRADE_COMPARE_AS(out.str(), "TextureTools::AtlasLandfill::setFlags(): only one of RotatePortrait and RotateLandscape can be set\n" - "TextureTools::AtlasLandfillArray::setFlags(): only one of RotatePortrait and RotateLandscape can be set\n" - "TextureTools::AtlasLandfill::setFlags(): only one of WidestFirst and NarrowestFirst can be set\n" - "TextureTools::AtlasLandfillArray::setFlags(): only one of WidestFirst and NarrowestFirst can be set\n", + "TextureTools::AtlasLandfill::setFlags(): only one of WidestFirst and NarrowestFirst can be set\n", TestSuite::Compare::String); } void AtlasTest::landfillAddMissingRotations() { CORRADE_SKIP_IF_NO_ASSERT(); - AtlasLandfill atlasPortrait{{16, 23}}; - AtlasLandfill atlasLandscape{{16, 23}}; - AtlasLandfillArray arrayPortrait{{16, 23, 2}}; - AtlasLandfillArray arrayLandscape{{16, 23, 2}}; - atlasPortrait.setFlags(AtlasLandfillFlag::RotatePortrait); - arrayPortrait.setFlags(AtlasLandfillFlag::RotatePortrait); - atlasLandscape.setFlags(AtlasLandfillFlag::RotateLandscape); - arrayLandscape.setFlags(AtlasLandfillFlag::RotateLandscape); + AtlasLandfill portrait{{16, 23}}; + AtlasLandfill landscape{{16, 23}}; + portrait.setFlags(AtlasLandfillFlag::RotatePortrait); + landscape.setFlags(AtlasLandfillFlag::RotateLandscape); Vector2i sizes[2]; Vector2i offsets[2]; Vector3i offsets3[2]; std::ostringstream out; Error redirectError{&out}; - atlasPortrait.add(sizes, offsets); - arrayPortrait.add(sizes, offsets3); + portrait.add(sizes, offsets); + portrait.add(sizes, offsets3); /* "Testing" the rotation-less init list variants too */ - atlasLandscape.add({{}, {}}, offsets); - arrayLandscape.add({{}, {}}, offsets3); + landscape.add({{}, {}}, offsets); + landscape.add({{}, {}}, offsets3); CORRADE_COMPARE(out.str(), "TextureTools::AtlasLandfill::add(): TextureTools::AtlasLandfillFlag::RotatePortrait set, expected a rotations view\n" - "TextureTools::AtlasLandfillArray::add(): TextureTools::AtlasLandfillFlag::RotatePortrait set, expected a rotations view\n" + "TextureTools::AtlasLandfill::add(): TextureTools::AtlasLandfillFlag::RotatePortrait set, expected a rotations view\n" "TextureTools::AtlasLandfill::add(): TextureTools::AtlasLandfillFlag::RotateLandscape set, expected a rotations view\n" - "TextureTools::AtlasLandfillArray::add(): TextureTools::AtlasLandfillFlag::RotateLandscape set, expected a rotations view\n"); + "TextureTools::AtlasLandfill::add(): TextureTools::AtlasLandfillFlag::RotateLandscape set, expected a rotations view\n"); } void AtlasTest::landfillAddInvalidViewSizes() { @@ -1095,16 +1059,38 @@ void AtlasTest::landfillAddInvalidViewSizes() { "TextureTools::AtlasLandfill::add(): expected sizes and rotations views to have the same size, got 2 and 3\n"); } +void AtlasTest::landfillAddTwoComponentForArray() { + CORRADE_SKIP_IF_NO_ASSERT(); + + AtlasLandfill atlas{{16, 23, 3}}; + atlas.clearFlags(AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape); + Vector2i sizes[2]; + Vector2i offsets[2]; + UnsignedByte rotationsData[1]; + Containers::MutableBitArrayView rotations{rotationsData, 0, 2}; + + std::ostringstream out; + Error redirectError{&out}; + atlas.add(sizes, offsets, rotations); + atlas.add(sizes, offsets); + atlas.add({}, offsets, rotations); + atlas.add({}, offsets); + CORRADE_COMPARE(out.str(), + "TextureTools::AtlasLandfill::add(): use the three-component overload for an array atlas\n" + "TextureTools::AtlasLandfill::add(): use the three-component overload for an array atlas\n" + "TextureTools::AtlasLandfill::add(): use the three-component overload for an array atlas\n" + "TextureTools::AtlasLandfill::add(): use the three-component overload for an array atlas\n"); +} + void AtlasTest::landfillAddTooLargeElement() { CORRADE_SKIP_IF_NO_ASSERT(); - /* The atlas makes the sizes portrait first, the array landscape instead */ - AtlasLandfill atlas{{16, 23}}; - AtlasLandfill atlas2{{16, 13}}; - AtlasLandfillArray array{{23, 16, 3}}; - AtlasLandfillArray array2{{13, 16, 3}}; - array.setFlags(AtlasLandfillFlag::RotateLandscape); - array2.setFlags(AtlasLandfillFlag::RotateLandscape); + AtlasLandfill portrait{{16, 23}}; + AtlasLandfill portrait2{{16, 13}}; + AtlasLandfill landscape{{23, 16}}; + AtlasLandfill landscape2{{13, 16}}; + landscape.setFlags(AtlasLandfillFlag::RotateLandscape); + landscape2.setFlags(AtlasLandfillFlag::RotateLandscape); Vector2i offsets[2]; Vector3i offsets3[2]; UnsignedByte rotationsData[1]; @@ -1112,20 +1098,20 @@ void AtlasTest::landfillAddTooLargeElement() { std::ostringstream out; Error redirectError{&out}; - atlas.add({{16, 23}, {0, 23}}, offsets, rotations); - array.add({{23, 16}, {23, 0}}, offsets3, rotations); - atlas.add({{16, 23}, {17, 23}}, offsets, rotations); - array.add({{23, 16}, {23, 17}}, offsets3, rotations); + portrait.add({{16, 23}, {0, 23}}, offsets, rotations); + landscape.add({{23, 16}, {23, 0}}, offsets3, rotations); + portrait.add({{16, 23}, {17, 23}}, offsets, rotations); + landscape.add({{23, 16}, {23, 17}}, offsets3, rotations); /* Sizes that fit but don't after a flip */ - atlas2.add({{13, 13}, {15, 13}}, offsets, rotations); - array2.add({{13, 13}, {13, 15}}, offsets3, rotations); + portrait2.add({{13, 13}, {15, 13}}, offsets, rotations); + landscape2.add({{13, 13}, {13, 15}}, offsets3, rotations); CORRADE_COMPARE_AS(out.str(), "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 23} but got {0, 23}\n" - "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {23, 0}\n" + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {23, 0}\n" "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 23} but got {17, 23}\n" - "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {23, 17}\n" + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {23, 17}\n" "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 13} but got {13, 15}\n" - "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {13, 16} but got {15, 13}\n", + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {13, 16} but got {15, 13}\n", TestSuite::Compare::String); } @@ -1134,16 +1120,15 @@ void AtlasTest::landfillAddTooLargeElementPadded() { CORRADE_SKIP_IF_NO_ASSERT(); - /* The atlas makes the sizes portrait first, the array landscape instead */ - AtlasLandfill atlas{{16, 23}}; - AtlasLandfill atlas2{{16, 13}}; - AtlasLandfillArray array{{23, 16, 3}}; - AtlasLandfillArray array2{{13, 16, 3}}; - atlas.setPadding({2, 1}); - atlas2.setPadding({2, 1}); - array.setPadding({1, 2}) + AtlasLandfill portrait{{16, 23}}; + AtlasLandfill portrait2{{16, 13}}; + AtlasLandfill landscape{{23, 16}}; + AtlasLandfill landscape2{{13, 16}}; + portrait.setPadding({2, 1}); + portrait2.setPadding({2, 1}); + landscape.setPadding({1, 2}) .setFlags(AtlasLandfillFlag::RotateLandscape); - array2.setPadding({1, 2}) + landscape2.setPadding({1, 2}) .setFlags(AtlasLandfillFlag::RotateLandscape); Vector2i offsets[2]; Vector3i offsets3[2]; @@ -1152,20 +1137,20 @@ void AtlasTest::landfillAddTooLargeElementPadded() { std::ostringstream out; Error redirectError{&out}; - atlas.add({{12, 21}, {0, 21}}, offsets, rotations); - array.add({{21, 12}, {21, 0}}, offsets3, rotations); - atlas.add({{12, 21}, {13, 21}}, offsets, rotations); - array.add({{21, 12}, {21, 13}}, offsets3, rotations); + portrait.add({{12, 21}, {0, 21}}, offsets, rotations); + landscape.add({{21, 12}, {21, 0}}, offsets3, rotations); + portrait.add({{12, 21}, {13, 21}}, offsets, rotations); + landscape.add({{21, 12}, {21, 13}}, offsets3, rotations); /* Sizes that fit but don't after a flip */ - atlas2.add({{9, 11}, {12, 11}}, offsets, rotations); - array2.add({{11, 9}, {11, 12}}, offsets3, rotations); + portrait2.add({{9, 11}, {12, 11}}, offsets, rotations); + landscape2.add({{11, 9}, {11, 12}}, offsets3, rotations); CORRADE_COMPARE_AS(out.str(), "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 23} but got {0, 21} and padding {2, 1}\n" - "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {21, 0} and padding {1, 2}\n" + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {21, 0} and padding {1, 2}\n" "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 23} but got {13, 21} and padding {2, 1}\n" - "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {21, 13} and padding {1, 2}\n" + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {21, 13} and padding {1, 2}\n" "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 13} but got {11, 12} and padding {1, 2}\n" - "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {13, 16} but got {12, 11} and padding {2, 1}\n", + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {13, 16} but got {12, 11} and padding {2, 1}\n", TestSuite::Compare::String); }