Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions include/scratchcpp/ispritehandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ class LIBSCRATCHCPP_EXPORT ISpriteHandler

/*! Called when the rotation style changes. */
virtual void onRotationStyleChanged(Sprite::RotationStyle rotationStyle) = 0;

/*!
* Used to get the bounding rectangle of the sprite.
* \note The rectangle must be relative to the stage, so make sure to use the sprite's coordinates.
*/
virtual Rect boundingRect() const = 0;
};

} // namespace libscratchcpp
4 changes: 2 additions & 2 deletions src/blocks/motionblocks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -653,8 +653,8 @@ unsigned int MotionBlocks::ifOnEdgeBounce(VirtualMachine *vm)

// Measure distance to edges
// Values are zero when the sprite is beyond
unsigned int stageWidth = engine->stageWidth();
unsigned int stageHeight = engine->stageHeight();
double stageWidth = engine->stageWidth();
double stageHeight = engine->stageHeight();
double distLeft = std::max(0.0, (stageWidth / 2.0) + bounds.left());
double distTop = std::max(0.0, (stageHeight / 2.0) - bounds.top());
double distRight = std::max(0.0, (stageWidth / 2.0) - bounds.right());
Expand Down
9 changes: 4 additions & 5 deletions src/scratch/sprite.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -350,10 +350,10 @@ void Sprite::setRotationStyle(const char *newRotationStyle)
/*! Returns the bounding rectangle of the sprite. */
Rect Sprite::boundingRect() const
{
Rect ret;
impl->getBoundingRect(&ret);
if (!impl->iface)
return Rect();

return ret;
return impl->iface->boundingRect();
}

/*!
Expand All @@ -374,8 +374,7 @@ void Sprite::keepInFence(double newX, double newY, double *fencedX, double *fenc
double stageWidth = eng->stageWidth();
double stageHeight = eng->stageHeight();
Rect fence(-stageWidth / 2, stageHeight / 2, stageWidth / 2, -stageHeight / 2);
Rect bounds;
impl->getBoundingRect(&bounds);
Rect bounds = boundingRect();

// Adjust the known bounds to the target position
bounds.setLeft(bounds.left() + newX - impl->x);
Expand Down
64 changes: 6 additions & 58 deletions src/scratch/sprite_p.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,61 +31,6 @@ void SpritePrivate::removeClone(Sprite *clone)
}
}

void SpritePrivate::getBoundingRect(Rect *out) const
{
assert(out);
assert(sprite);
auto costume = sprite->currentCostume();

if (!costume) {
out->setLeft(x);
out->setTop(y);
out->setRight(x);
out->setBottom(y);
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);
}

void SpritePrivate::getFencedPosition(double x, double y, double *outX, double *outY) const
{
assert(outX);
Expand All @@ -101,10 +46,13 @@ void SpritePrivate::getFencedPosition(double x, double y, double *outX, double *
double dx = x - this->x;
double dy = y - this->y;
Rect rect;
getBoundingRect(&rect);

if (iface)
rect = iface->boundingRect();

double inset = std::floor(std::min(rect.width(), rect.height()) / 2);

double xRight = sprite->engine()->stageWidth() / 2;
double xRight = static_cast<double>(sprite->engine()->stageWidth()) / 2;
double sx = xRight - std::min(FENCE_WIDTH, inset);

if (rect.right() + dx < -sx) {
Expand All @@ -113,7 +61,7 @@ void SpritePrivate::getFencedPosition(double x, double y, double *outX, double *
x = std::floor(this->x + (sx - rect.left()));
}

double yTop = sprite->engine()->stageHeight() / 2;
double yTop = static_cast<double>(sprite->engine()->stageHeight()) / 2;
double sy = yTop - std::min(FENCE_WIDTH, inset);

if (rect.top() + dy < -sy) {
Expand Down
1 change: 0 additions & 1 deletion src/scratch/sprite_p.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ struct SpritePrivate

void removeClone(Sprite *clone);

void getBoundingRect(Rect *out) const;
void getFencedPosition(double inX, double inY, double *outX, double *outY) const;

Sprite *sprite = nullptr;
Expand Down
72 changes: 29 additions & 43 deletions test/blocks/motion_blocks_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
#include <scratchcpp/field.h>
#include <scratchcpp/sprite.h>
#include <scratchcpp/costume.h>
#include <scratchcpp/scratchconfiguration.h>
#include <scratchcpp/rect.h>
#include <enginemock.h>
#include <randomgeneratormock.h>
#include <clockmock.h>
#include <imageformatfactorymock.h>
#include <imageformatmock.h>
#include <spritehandlermock.h>

#include "../common.h"
#include "blocks/motionblocks.h"
Expand Down Expand Up @@ -1117,54 +1116,27 @@ TEST_F(MotionBlocksTest, IfOnEdgeBounceImpl)
static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT };
static BlockFunc functions[] = { &MotionBlocks::ifOnEdgeBounce };

auto imageFormatFactory = std::make_shared<ImageFormatFactoryMock>();
auto imageFormat = std::make_shared<ImageFormatMock>();

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<Costume>("costume1", "a", "test");

Sprite sprite;
sprite.addCostume(costume);
sprite.setCostumeIndex(0);

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);
SpriteHandlerMock handler;
EXPECT_CALL(handler, init);
sprite.setInterface(&handler);

sprite.setEngine(&m_engineMock);

VirtualMachine vm(&sprite, &m_engineMock, nullptr);
vm.setBytecode(bytecode);
vm.setFunctions(functions);

EXPECT_CALL(*imageFormat, width()).Times(9).WillRepeatedly(Return(4));
EXPECT_CALL(*imageFormat, height()).Times(9).WillRepeatedly(Return(3));
EXPECT_CALL(m_engineMock, stageWidth()).Times(9).WillRepeatedly(Return(480));
EXPECT_CALL(m_engineMock, stageHeight()).Times(9).WillRepeatedly(Return(360));

// No edge
EXPECT_CALL(m_engineMock, requestRedraw()).Times(3);
EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(2).WillRepeatedly(Return(false));
EXPECT_CALL(handler, onXChanged);
EXPECT_CALL(handler, onYChanged);
EXPECT_CALL(handler, onDirectionChanged);
EXPECT_CALL(handler, boundingRect()).WillOnce(Return(Rect(80, 80, 120, 40)));
sprite.setX(100);
sprite.setY(60);
sprite.setDirection(-45);
Expand All @@ -1178,19 +1150,27 @@ TEST_F(MotionBlocksTest, IfOnEdgeBounceImpl)
// Left edge
EXPECT_CALL(m_engineMock, requestRedraw()).Times(5);
EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
EXPECT_CALL(handler, onXChanged).Times(2);
EXPECT_CALL(handler, onYChanged).Times(2);
EXPECT_CALL(handler, onDirectionChanged);
EXPECT_CALL(handler, boundingRect()).Times(2).WillRepeatedly(Return(Rect(-260, 80, -220, 40)));
sprite.setX(-240);
sprite.setY(60);
vm.reset();
vm.run();

ASSERT_EQ(vm.registerCount(), 0);
ASSERT_EQ(std::round(sprite.x() * 100) / 100, -238.23);
ASSERT_EQ(std::round(sprite.x() * 100) / 100, -220);
ASSERT_EQ(sprite.y(), 60);
ASSERT_EQ(std::round(sprite.direction() * 100) / 100, 45);

// Top edge
EXPECT_CALL(m_engineMock, requestRedraw()).Times(6);
EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
EXPECT_CALL(handler, onXChanged).Times(2);
EXPECT_CALL(handler, onYChanged).Times(2);
EXPECT_CALL(handler, onDirectionChanged).Times(2);
EXPECT_CALL(handler, boundingRect()).Times(2).WillRepeatedly(Return(Rect(80, 200, 120, 160)));
sprite.setX(100);
sprite.setY(180);
sprite.setDirection(45);
Expand All @@ -1199,36 +1179,42 @@ TEST_F(MotionBlocksTest, IfOnEdgeBounceImpl)

ASSERT_EQ(vm.registerCount(), 0);
ASSERT_EQ(sprite.x(), 100);
ASSERT_EQ(std::round(sprite.y() * 100) / 100, 178.23);
ASSERT_EQ(std::round(sprite.y() * 100) / 100, 160);
ASSERT_EQ(sprite.direction(), 135);

// Right edge
EXPECT_CALL(m_engineMock, requestRedraw()).Times(5);
EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
EXPECT_CALL(handler, onXChanged).Times(2);
EXPECT_CALL(handler, onYChanged).Times(2);
EXPECT_CALL(handler, onDirectionChanged);
EXPECT_CALL(handler, boundingRect()).Times(2).WillRepeatedly(Return(Rect(220, 80, 260, 40)));
sprite.setX(240);
sprite.setY(60);
vm.reset();
vm.run();

ASSERT_EQ(vm.registerCount(), 0);
ASSERT_EQ(std::round(sprite.x() * 100) / 100, 238.23);
ASSERT_EQ(std::round(sprite.x() * 100) / 100, 220);
ASSERT_EQ(sprite.y(), 60);
ASSERT_EQ(sprite.direction(), -135);

// Bottom edge
EXPECT_CALL(m_engineMock, requestRedraw()).Times(5);
EXPECT_CALL(m_engineMock, spriteFencingEnabled()).Times(4).WillRepeatedly(Return(false));
EXPECT_CALL(handler, onXChanged).Times(2);
EXPECT_CALL(handler, onYChanged).Times(2);
EXPECT_CALL(handler, onDirectionChanged);
EXPECT_CALL(handler, boundingRect()).Times(2).WillRepeatedly(Return(Rect(-120, -160, -80, -200)));
sprite.setX(-100);
sprite.setY(-180);
vm.reset();
vm.run();

ASSERT_EQ(vm.registerCount(), 0);
ASSERT_EQ(sprite.x(), -100);
ASSERT_EQ(std::round(sprite.y() * 100) / 100, -178.23);
ASSERT_EQ(std::round(sprite.y() * 100) / 100, -160);
ASSERT_EQ(std::round(sprite.direction() * 100) / 100, -45);

ScratchConfiguration::removeImageFormat("test");
}

TEST_F(MotionBlocksTest, SetRotationStyle)
Expand Down
1 change: 1 addition & 0 deletions test/mocks/spritehandlermock.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ class SpriteHandlerMock : public ISpriteHandler
MOCK_METHOD(void, onSizeChanged, (double), (override));
MOCK_METHOD(void, onDirectionChanged, (double), (override));
MOCK_METHOD(void, onRotationStyleChanged, (Sprite::RotationStyle), (override));
MOCK_METHOD(Rect, boundingRect, (), (const, override));
};
Loading