Skip to content

Commit fb2805b

Browse files
committed
Add boundingRect method to Sprite
1 parent 16121d3 commit fb2805b

File tree

4 files changed

+180
-3
lines changed

4 files changed

+180
-3
lines changed

src/scratch/sprite.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <scratchcpp/variable.h>
77
#include <scratchcpp/list.h>
88
#include <scratchcpp/costume.h>
9+
#include <scratchcpp/rect.h>
910
#include <cassert>
1011

1112
#include "sprite_p.h"
@@ -15,7 +16,7 @@ using namespace libscratchcpp;
1516
/*! Constructs Sprite. */
1617
Sprite::Sprite() :
1718
Target(),
18-
impl(spimpl::make_unique_impl<SpritePrivate>())
19+
impl(spimpl::make_unique_impl<SpritePrivate>(this))
1920
{
2021
}
2122

@@ -297,6 +298,15 @@ void Sprite::setRotationStyle(const char *newRotationStyle)
297298
setRotationStyle(std::string(newRotationStyle));
298299
}
299300

301+
/*! Returns the bounding rectangle of the sprite. */
302+
Rect Sprite::boundingRect() const
303+
{
304+
Rect ret;
305+
impl->getBoundingRect(&ret);
306+
307+
return ret;
308+
}
309+
300310
Target *Sprite::dataSource() const
301311
{
302312
return impl->cloneRoot;

src/scratch/sprite_p.cpp

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
// SPDX-License-Identifier: Apache-2.0
22

33
#include <scratchcpp/ispritehandler.h>
4+
#include <scratchcpp/rect.h>
5+
#include <scratchcpp/costume.h>
6+
#include <cmath>
47

58
#include "sprite_p.h"
69

710
using namespace libscratchcpp;
811

9-
SpritePrivate::SpritePrivate()
12+
static const double pi = std::acos(-1); // TODO: Use std::numbers::pi in C++20
13+
14+
SpritePrivate::SpritePrivate(Sprite *sprite) :
15+
sprite(sprite)
1016
{
1117
}
1218

@@ -28,3 +34,59 @@ void SpritePrivate::setCostumeData(const char *data)
2834
if (iface)
2935
iface->onCostumeChanged(data);
3036
}
37+
38+
void SpritePrivate::getBoundingRect(Rect *out) const
39+
{
40+
assert(out);
41+
assert(sprite);
42+
// TODO: Make currentCostume() return the costume
43+
auto costume = sprite->costumeAt(sprite->currentCostume() - 1);
44+
45+
if (!costume) {
46+
out->setLeft(0);
47+
out->setTop(0);
48+
out->setRight(0);
49+
out->setBottom(0);
50+
return;
51+
}
52+
53+
double cosTheta = std::cos((90 - direction) * pi / 180);
54+
double sinTheta = std::sin((90 - direction) * pi / 180);
55+
double maxX = 0, maxY = 0, minX = 0, minY = 0;
56+
bool firstPixel = true;
57+
unsigned int width = costume->width();
58+
unsigned int height = costume->height();
59+
double rotationCenterX = width / 2.0 + costume->rotationCenterX();
60+
double rotationCenterY = height / 2.0 + costume->rotationCenterY();
61+
Rgb **bitmap = costume->bitmap();
62+
63+
for (unsigned int y = 0; y < height; y++) {
64+
for (unsigned int x = 0; x < width; x++) {
65+
if (bitmap[y][x] != rgba(0, 0, 0, 0)) {
66+
double rotatedX = ((x - rotationCenterX) * cosTheta - (y - rotationCenterY) * sinTheta);
67+
double rotatedY = ((x - rotationCenterX) * sinTheta + (y - rotationCenterY) * cosTheta);
68+
69+
if (firstPixel) {
70+
firstPixel = false;
71+
minX = maxX = rotatedX;
72+
minY = maxY = rotatedY;
73+
} else {
74+
if (rotatedX < minX)
75+
minX = rotatedX;
76+
else if (rotatedX > maxX)
77+
maxX = rotatedX;
78+
79+
if (rotatedY < minY)
80+
minY = rotatedY;
81+
else if (rotatedY > maxY)
82+
maxY = rotatedY;
83+
}
84+
}
85+
}
86+
}
87+
88+
out->setLeft(x + minX);
89+
out->setTop(y + maxY);
90+
out->setRight(x + maxX);
91+
out->setBottom(y + minY);
92+
}

src/scratch/sprite_p.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@
77
namespace libscratchcpp
88
{
99

10+
class Rect;
11+
1012
struct SpritePrivate
1113
{
12-
SpritePrivate();
14+
SpritePrivate(Sprite *sprite);
1315
SpritePrivate(const SpritePrivate &) = delete;
1416

1517
void removeClone(Sprite *clone);
1618

1719
void setCostumeData(const char *data);
20+
void getBoundingRect(Rect *out) const;
1821

22+
Sprite *sprite = nullptr;
1923
ISpriteHandler *iface = nullptr;
2024
Sprite *cloneRoot = nullptr;
2125
Sprite *cloneParent = nullptr;

test/scratch_classes/sprite_test.cpp

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22
#include <scratchcpp/variable.h>
33
#include <scratchcpp/list.h>
44
#include <scratchcpp/costume.h>
5+
#include <scratchcpp/scratchconfiguration.h>
6+
#include <scratchcpp/rect.h>
57
#include <enginemock.h>
8+
#include <imageformatfactorymock.h>
9+
#include <imageformatmock.h>
610

711
#include "../common.h"
812

913
using namespace libscratchcpp;
1014

1115
using ::testing::_;
1216
using ::testing::SaveArg;
17+
using ::testing::Return;
1318

1419
TEST(SpriteTest, IsStage)
1520
{
@@ -375,3 +380,99 @@ TEST(SpriteTest, RotationStyle)
375380
ASSERT_EQ(sprite.rotationStyleStr(), "all around");
376381
ASSERT_EQ(c2->mirrorHorizontally(), false);
377382
}
383+
384+
TEST(SpriteTest, BoundingRect)
385+
{
386+
auto imageFormatFactory = std::make_shared<ImageFormatFactoryMock>();
387+
auto imageFormat = std::make_shared<ImageFormatMock>();
388+
389+
ScratchConfiguration::registerImageFormat("test", imageFormatFactory);
390+
EXPECT_CALL(*imageFormatFactory, createInstance()).WillOnce(Return(imageFormat));
391+
EXPECT_CALL(*imageFormat, width()).WillOnce(Return(0));
392+
EXPECT_CALL(*imageFormat, height()).WillOnce(Return(0));
393+
auto costume = std::make_shared<Costume>("costume1", "a", "test");
394+
395+
Sprite sprite;
396+
sprite.addCostume(costume);
397+
sprite.setCurrentCostume(1);
398+
399+
static char data[5] = "abcd";
400+
EXPECT_CALL(*imageFormat, setData(5, data));
401+
EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
402+
EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
403+
404+
EXPECT_CALL(*imageFormat, colorAt(0, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
405+
EXPECT_CALL(*imageFormat, colorAt(1, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
406+
EXPECT_CALL(*imageFormat, colorAt(2, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
407+
EXPECT_CALL(*imageFormat, colorAt(3, 0, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
408+
409+
EXPECT_CALL(*imageFormat, colorAt(0, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
410+
EXPECT_CALL(*imageFormat, colorAt(1, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
411+
EXPECT_CALL(*imageFormat, colorAt(2, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
412+
EXPECT_CALL(*imageFormat, colorAt(3, 1, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
413+
414+
EXPECT_CALL(*imageFormat, colorAt(0, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 255)));
415+
EXPECT_CALL(*imageFormat, colorAt(1, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
416+
EXPECT_CALL(*imageFormat, colorAt(2, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
417+
EXPECT_CALL(*imageFormat, colorAt(3, 2, 1)).WillOnce(Return(rgba(0, 0, 0, 0)));
418+
costume->setData(5, data);
419+
420+
EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
421+
EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
422+
Rect rect = sprite.boundingRect();
423+
ASSERT_EQ(rect.left(), -2);
424+
ASSERT_EQ(rect.top(), 0.5);
425+
ASSERT_EQ(rect.right(), 1);
426+
ASSERT_EQ(rect.bottom(), -1.5);
427+
428+
sprite.setDirection(45);
429+
EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
430+
EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
431+
rect = sprite.boundingRect();
432+
ASSERT_EQ(std::round(rect.left() * 10000) / 10000, -1.7678);
433+
ASSERT_EQ(std::round(rect.top() * 10000) / 10000, 0.3536);
434+
ASSERT_EQ(std::round(rect.right() * 10000) / 10000, 1.0607);
435+
ASSERT_EQ(std::round(rect.bottom() * 10000) / 10000, -1.0607);
436+
437+
sprite.setDirection(-160);
438+
EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
439+
EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
440+
rect = sprite.boundingRect();
441+
ASSERT_EQ(std::round(rect.left() * 10000) / 10000, -1.4095);
442+
ASSERT_EQ(std::round(rect.top() * 10000) / 10000, 1.7084);
443+
ASSERT_EQ(std::round(rect.right() * 10000) / 10000, 1.1539);
444+
ASSERT_EQ(std::round(rect.bottom() * 10000) / 10000, -0.7687);
445+
446+
sprite.setX(86.48);
447+
sprite.setY(-147.16);
448+
EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
449+
EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
450+
rect = sprite.boundingRect();
451+
ASSERT_EQ(std::round(rect.left() * 10000) / 10000, 85.0705);
452+
ASSERT_EQ(std::round(rect.top() * 10000) / 10000, -145.4516);
453+
ASSERT_EQ(std::round(rect.right() * 10000) / 10000, 87.6339);
454+
ASSERT_EQ(std::round(rect.bottom() * 10000) / 10000, -147.9287);
455+
456+
costume->setRotationCenterX(-4);
457+
costume->setRotationCenterY(8);
458+
EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
459+
EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
460+
rect = sprite.boundingRect();
461+
ASSERT_EQ(std::round(rect.left() * 10000) / 10000, 76.1848);
462+
ASSERT_EQ(std::round(rect.top() * 10000) / 10000, -146.4742);
463+
ASSERT_EQ(std::round(rect.right() * 10000) / 10000, 78.7483);
464+
ASSERT_EQ(std::round(rect.bottom() * 10000) / 10000, -148.9513);
465+
466+
sprite.setDirection(90);
467+
sprite.setX(0);
468+
sprite.setY(0);
469+
EXPECT_CALL(*imageFormat, width()).WillOnce(Return(4));
470+
EXPECT_CALL(*imageFormat, height()).WillOnce(Return(3));
471+
rect = sprite.boundingRect();
472+
ASSERT_EQ(rect.left(), 2);
473+
ASSERT_EQ(rect.top(), -7.5);
474+
ASSERT_EQ(rect.right(), 5);
475+
ASSERT_EQ(rect.bottom(), -9.5);
476+
477+
ScratchConfiguration::removeImageFormat("test");
478+
}

0 commit comments

Comments
 (0)