diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a4987e0..39f6a817 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ target_sources(scratchcpp include/scratchcpp/keyevent.h include/scratchcpp/iimageformat.h include/scratchcpp/iimageformatfactory.h + include/scratchcpp/rect.h ) add_library(zip SHARED diff --git a/include/scratchcpp/rect.h b/include/scratchcpp/rect.h new file mode 100644 index 00000000..6a73576e --- /dev/null +++ b/include/scratchcpp/rect.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "global.h" +#include "spimpl.h" + +namespace libscratchcpp +{ + +class RectPrivate; + +/*! The Rect class represents a rectangle. */ +class LIBSCRATCHCPP_EXPORT Rect +{ + public: + Rect(double left, double top, double right, double bottom); + Rect(); + + double left() const; + void setLeft(double left); + + double top() const; + void setTop(double top); + + double right() const; + void setRight(double right); + + double bottom() const; + void setBottom(double bottom); + + double width() const; + double height() const; + + private: + spimpl::impl_ptr impl; +}; + +} // namespace libscratchcpp diff --git a/include/scratchcpp/sprite.h b/include/scratchcpp/sprite.h index 48c508ca..3d669254 100644 --- a/include/scratchcpp/sprite.h +++ b/include/scratchcpp/sprite.h @@ -8,6 +8,7 @@ namespace libscratchcpp { class ISpriteHandler; +class Rect; class SpritePrivate; /*! \brief The Sprite class represents a Scratch sprite. */ @@ -62,6 +63,8 @@ class LIBSCRATCHCPP_EXPORT Sprite : public Target void setRotationStyle(const std::string &newRotationStyle); void setRotationStyle(const char *newRotationStyle); + Rect boundingRect() const; + private: Target *dataSource() const override; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9a920930..d544e200 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,6 +6,9 @@ target_sources(scratchcpp scratchconfiguration.cpp scratchconfiguration_p.cpp scratchconfiguration_p.h + rect.cpp + rect_p.cpp + rect_p.h ) add_subdirectory(blocks) diff --git a/src/rect.cpp b/src/rect.cpp new file mode 100644 index 00000000..7e93f08d --- /dev/null +++ b/src/rect.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include "rect_p.h" + +using namespace libscratchcpp; + +/*! Constructs Rect. */ +Rect::Rect(double left, double top, double right, double bottom) : + impl(spimpl::make_impl(left, top, right, bottom)) +{ +} + +/*! \copydoc Rect() */ +Rect::Rect() : + impl(spimpl::make_impl()) +{ +} + +/*! Returns the x-coordinate of the left edge. */ +double Rect::left() const +{ + return impl->left; +} + +/*! Sets the x-coordinate of the left edge. */ +void Rect::setLeft(double left) +{ + impl->left = left; +} + +/*! Returns the y-coordinate of the top edge. */ +double Rect::top() const +{ + return impl->top; +} + +/*! Sets the y-coordinate of the top edge. */ +void Rect::setTop(double top) +{ + impl->top = top; +} + +/*! Returns the x-coordinate of the right edge. */ +double Rect::right() const +{ + return impl->right; +} + +/*! Sets the x-coordinate of the right edge. */ +void Rect::setRight(double right) +{ + impl->right = right; +} + +/*! Returns the y-coordinate of the bottom edge. */ +double Rect::bottom() const +{ + return impl->bottom; +} + +/*! Sets the y-coordinate of the bottom edge. */ +void Rect::setBottom(double bottom) +{ + impl->bottom = bottom; +} + +/*! Returns the width of the rectangle. */ +double Rect::width() const +{ + return std::abs(impl->right - impl->left); +} + +/*! Returns the height of the rectangle. */ +double Rect::height() const +{ + return std::abs(impl->top - impl->bottom); +} diff --git a/src/rect_p.cpp b/src/rect_p.cpp new file mode 100644 index 00000000..b69464f7 --- /dev/null +++ b/src/rect_p.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "rect_p.h" + +namespace libscratchcpp +{ + +RectPrivate::RectPrivate(double left, double top, double right, double bottom) : + left(left), + top(top), + right(right), + bottom(bottom) +{ +} + +RectPrivate::RectPrivate() +{ +} + +} // namespace libscratchcpp diff --git a/src/rect_p.h b/src/rect_p.h new file mode 100644 index 00000000..cf451544 --- /dev/null +++ b/src/rect_p.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +namespace libscratchcpp +{ + +struct RectPrivate +{ + RectPrivate(double left, double top, double right, double bottom); + RectPrivate(); + + double left = 0; + double top = 0; + double right = 0; + double bottom = 0; +}; + +} // namespace libscratchcpp diff --git a/src/scratch/sprite.cpp b/src/scratch/sprite.cpp index cb5351d7..9cc6ae15 100644 --- a/src/scratch/sprite.cpp +++ b/src/scratch/sprite.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "sprite_p.h" @@ -15,7 +16,7 @@ using namespace libscratchcpp; /*! Constructs Sprite. */ Sprite::Sprite() : Target(), - impl(spimpl::make_unique_impl()) + impl(spimpl::make_unique_impl(this)) { } @@ -297,6 +298,15 @@ void Sprite::setRotationStyle(const char *newRotationStyle) setRotationStyle(std::string(newRotationStyle)); } +/*! Returns the bounding rectangle of the sprite. */ +Rect Sprite::boundingRect() const +{ + Rect ret; + impl->getBoundingRect(&ret); + + return ret; +} + Target *Sprite::dataSource() const { return impl->cloneRoot; diff --git a/src/scratch/sprite_p.cpp b/src/scratch/sprite_p.cpp index 22267c42..d9fca449 100644 --- a/src/scratch/sprite_p.cpp +++ b/src/scratch/sprite_p.cpp @@ -1,12 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include +#include +#include #include "sprite_p.h" using namespace libscratchcpp; -SpritePrivate::SpritePrivate() +static const double pi = std::acos(-1); // TODO: Use std::numbers::pi in C++20 + +SpritePrivate::SpritePrivate(Sprite *sprite) : + sprite(sprite) { } @@ -28,3 +34,59 @@ void SpritePrivate::setCostumeData(const char *data) if (iface) iface->onCostumeChanged(data); } + +void SpritePrivate::getBoundingRect(Rect *out) const +{ + assert(out); + assert(sprite); + // TODO: Make currentCostume() return the costume + auto costume = sprite->costumeAt(sprite->currentCostume() - 1); + + if (!costume) { + out->setLeft(0); + out->setTop(0); + out->setRight(0); + out->setBottom(0); + return; + } + + double cosTheta = std::cos((90 - direction) * pi / 180); + double sinTheta = std::sin((90 - direction) * pi / 180); + double maxX = 0, maxY = 0, minX = 0, minY = 0; + bool firstPixel = true; + unsigned int width = costume->width(); + unsigned int height = costume->height(); + double rotationCenterX = width / 2.0 + costume->rotationCenterX(); + double rotationCenterY = height / 2.0 + costume->rotationCenterY(); + Rgb **bitmap = costume->bitmap(); + + for (unsigned int y = 0; y < height; y++) { + for (unsigned int x = 0; x < width; x++) { + if (bitmap[y][x] != rgba(0, 0, 0, 0)) { + double rotatedX = ((x - rotationCenterX) * cosTheta - (y - rotationCenterY) * sinTheta); + double rotatedY = ((x - rotationCenterX) * sinTheta + (y - rotationCenterY) * cosTheta); + + if (firstPixel) { + firstPixel = false; + minX = maxX = rotatedX; + minY = maxY = rotatedY; + } else { + if (rotatedX < minX) + minX = rotatedX; + else if (rotatedX > maxX) + maxX = rotatedX; + + if (rotatedY < minY) + minY = rotatedY; + else if (rotatedY > maxY) + maxY = rotatedY; + } + } + } + } + + out->setLeft(x + minX); + out->setTop(y + maxY); + out->setRight(x + maxX); + out->setBottom(y + minY); +} diff --git a/src/scratch/sprite_p.h b/src/scratch/sprite_p.h index 84ca01ea..fa86987c 100644 --- a/src/scratch/sprite_p.h +++ b/src/scratch/sprite_p.h @@ -7,15 +7,19 @@ namespace libscratchcpp { +class Rect; + struct SpritePrivate { - SpritePrivate(); + SpritePrivate(Sprite *sprite); SpritePrivate(const SpritePrivate &) = delete; void removeClone(Sprite *clone); void setCostumeData(const char *data); + void getBoundingRect(Rect *out) const; + Sprite *sprite = nullptr; ISpriteHandler *iface = nullptr; Sprite *cloneRoot = nullptr; Sprite *cloneParent = nullptr; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7ce4a85e..bd8f3190 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -32,3 +32,4 @@ add_subdirectory(clock) add_subdirectory(timer) add_subdirectory(randomgenerator) add_subdirectory(imageformats) +add_subdirectory(rect) diff --git a/test/rect/CMakeLists.txt b/test/rect/CMakeLists.txt new file mode 100644 index 00000000..74d62043 --- /dev/null +++ b/test/rect/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable( + rect_test + rect_test.cpp +) + +target_link_libraries( + rect_test + GTest::gtest_main + scratchcpp +) + +gtest_discover_tests(rect_test) diff --git a/test/rect/rect_test.cpp b/test/rect/rect_test.cpp new file mode 100644 index 00000000..cdfc7515 --- /dev/null +++ b/test/rect/rect_test.cpp @@ -0,0 +1,79 @@ +#include +#include + +#include "../common.h" + +using namespace libscratchcpp; + +TEST(RectTest, Constructors) +{ + { + Rect rect; + ASSERT_EQ(rect.left(), 0); + ASSERT_EQ(rect.top(), 0); + ASSERT_EQ(rect.right(), 0); + ASSERT_EQ(rect.bottom(), 0); + ASSERT_EQ(rect.width(), 0); + ASSERT_EQ(rect.height(), 0); + } + + { + Rect rect(8.4, 150.78, 145.89, -179.99); + ASSERT_EQ(rect.left(), 8.4); + ASSERT_EQ(rect.top(), 150.78); + ASSERT_EQ(rect.right(), 145.89); + ASSERT_EQ(rect.bottom(), -179.99); + ASSERT_EQ(std::round(rect.width() * 100) / 100, 137.49); + ASSERT_EQ(rect.height(), 330.77); + } +} + +TEST(RectTest, Left) +{ + Rect rect; + + rect.setLeft(-78.05); + ASSERT_EQ(rect.left(), -78.05); +} + +TEST(RectTest, Top) +{ + Rect rect; + + rect.setTop(-22.89); + ASSERT_EQ(rect.top(), -22.89); +} + +TEST(RectTest, Right) +{ + Rect rect; + + rect.setRight(100.512); + ASSERT_EQ(rect.right(), 100.512); +} + +TEST(RectTest, Bottom) +{ + Rect rect; + + rect.setBottom(-58.162); + ASSERT_EQ(rect.bottom(), -58.162); +} + +TEST(RectTest, Width) +{ + Rect rect; + + rect.setLeft(-78.05); + rect.setRight(100.512); + ASSERT_EQ(rect.width(), 178.562); +} + +TEST(RectTest, Height) +{ + Rect rect; + + rect.setTop(-22.89); + rect.setBottom(-58.162); + ASSERT_EQ(rect.height(), 35.272); +} diff --git a/test/scratch_classes/sprite_test.cpp b/test/scratch_classes/sprite_test.cpp index d8186a0f..89df4cd5 100644 --- a/test/scratch_classes/sprite_test.cpp +++ b/test/scratch_classes/sprite_test.cpp @@ -2,7 +2,11 @@ #include #include #include +#include +#include #include +#include +#include #include "../common.h" @@ -10,6 +14,7 @@ using namespace libscratchcpp; using ::testing::_; using ::testing::SaveArg; +using ::testing::Return; TEST(SpriteTest, IsStage) { @@ -375,3 +380,99 @@ TEST(SpriteTest, RotationStyle) ASSERT_EQ(sprite.rotationStyleStr(), "all around"); ASSERT_EQ(c2->mirrorHorizontally(), false); } + +TEST(SpriteTest, BoundingRect) +{ + auto imageFormatFactory = std::make_shared(); + auto imageFormat = std::make_shared(); + + ScratchConfiguration::registerImageFormat("test", imageFormatFactory); + EXPECT_CALL(*imageFormatFactory, createInstance()).WillOnce(Return(imageFormat)); + EXPECT_CALL(*imageFormat, width()).WillOnce(Return(0)); + EXPECT_CALL(*imageFormat, height()).WillOnce(Return(0)); + auto costume = std::make_shared("costume1", "a", "test"); + + Sprite sprite; + sprite.addCostume(costume); + sprite.setCurrentCostume(1); + + static char data[5] = "abcd"; + EXPECT_CALL(*imageFormat, setData(5, data)); + EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4)); + EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3)); + + EXPECT_CALL(*imageFormat, colorAt(0, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0))); + EXPECT_CALL(*imageFormat, colorAt(1, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0))); + EXPECT_CALL(*imageFormat, colorAt(2, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 255))); + EXPECT_CALL(*imageFormat, colorAt(3, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0))); + + EXPECT_CALL(*imageFormat, colorAt(0, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 0))); + EXPECT_CALL(*imageFormat, colorAt(1, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 255))); + EXPECT_CALL(*imageFormat, colorAt(2, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 0))); + EXPECT_CALL(*imageFormat, colorAt(3, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 255))); + + EXPECT_CALL(*imageFormat, colorAt(0, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 255))); + EXPECT_CALL(*imageFormat, colorAt(1, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0))); + EXPECT_CALL(*imageFormat, colorAt(2, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0))); + EXPECT_CALL(*imageFormat, colorAt(3, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0))); + costume->setData(5, data); + + EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4)); + EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3)); + Rect rect = sprite.boundingRect(); + ASSERT_EQ(rect.left(), -2); + ASSERT_EQ(rect.top(), 0.5); + ASSERT_EQ(rect.right(), 1); + ASSERT_EQ(rect.bottom(), -1.5); + + sprite.setDirection(45); + EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4)); + EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3)); + rect = sprite.boundingRect(); + ASSERT_EQ(std::round(rect.left() * 10000) / 10000, -1.7678); + ASSERT_EQ(std::round(rect.top() * 10000) / 10000, 0.3536); + ASSERT_EQ(std::round(rect.right() * 10000) / 10000, 1.0607); + ASSERT_EQ(std::round(rect.bottom() * 10000) / 10000, -1.0607); + + sprite.setDirection(-160); + EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4)); + EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3)); + rect = sprite.boundingRect(); + ASSERT_EQ(std::round(rect.left() * 10000) / 10000, -1.4095); + ASSERT_EQ(std::round(rect.top() * 10000) / 10000, 1.7084); + ASSERT_EQ(std::round(rect.right() * 10000) / 10000, 1.1539); + ASSERT_EQ(std::round(rect.bottom() * 10000) / 10000, -0.7687); + + sprite.setX(86.48); + sprite.setY(-147.16); + EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4)); + EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3)); + rect = sprite.boundingRect(); + ASSERT_EQ(std::round(rect.left() * 10000) / 10000, 85.0705); + ASSERT_EQ(std::round(rect.top() * 10000) / 10000, -145.4516); + ASSERT_EQ(std::round(rect.right() * 10000) / 10000, 87.6339); + ASSERT_EQ(std::round(rect.bottom() * 10000) / 10000, -147.9287); + + costume->setRotationCenterX(-4); + costume->setRotationCenterY(8); + EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4)); + EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3)); + rect = sprite.boundingRect(); + ASSERT_EQ(std::round(rect.left() * 10000) / 10000, 76.1848); + ASSERT_EQ(std::round(rect.top() * 10000) / 10000, -146.4742); + ASSERT_EQ(std::round(rect.right() * 10000) / 10000, 78.7483); + ASSERT_EQ(std::round(rect.bottom() * 10000) / 10000, -148.9513); + + sprite.setDirection(90); + sprite.setX(0); + sprite.setY(0); + EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4)); + EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3)); + rect = sprite.boundingRect(); + ASSERT_EQ(rect.left(), 2); + ASSERT_EQ(rect.top(), -7.5); + ASSERT_EQ(rect.right(), 5); + ASSERT_EQ(rect.bottom(), -9.5); + + ScratchConfiguration::removeImageFormat("test"); +}