Skip to content

Commit

Permalink
Add touchingColor() with mask param to RenderedTarget
Browse files Browse the repository at this point in the history
  • Loading branch information
adazem009 committed Sep 10, 2024
1 parent 63dd35a commit 4b62855
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 37 deletions.
1 change: 1 addition & 0 deletions src/irenderedtarget.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class IRenderedTarget : public QNanoQuickItem

virtual bool touchingClones(const std::vector<libscratchcpp::Sprite *> &clones) const = 0;
virtual bool touchingColor(const libscratchcpp::Value &color) const = 0;
virtual bool touchingColor(const libscratchcpp::Value &color, const libscratchcpp::Value &mask) const = 0;
};

} // namespace scratchcpprender
111 changes: 75 additions & 36 deletions src/renderedtarget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -658,43 +658,12 @@ bool RenderedTarget::touchingClones(const std::vector<libscratchcpp::Sprite *> &

bool RenderedTarget::touchingColor(const Value &color) const
{
// https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L775-L841
if (!m_engine)
return false;

QRgb rgb = convertColor(color);

std::vector<Target *> targets;
m_engine->getVisibleTargets(targets);

QRectF myRect = touchingBounds();
std::vector<IRenderedTarget *> candidates;
QRectF bounds = candidatesBounds(myRect, targets, candidates);

if (colorMatches(rgb, qRgb(255, 255, 255))) {
// The color we're checking for is the background color which spans the entire stage
bounds = myRect;

if (bounds.isEmpty())
return false;
} else if (candidates.empty()) {
// If not checking for the background color, we can return early if there are no candidate drawables
return false;
}

// Loop through the points of the union
for (int y = bounds.top(); y <= bounds.bottom(); y++) {
for (int x = bounds.left(); x <= bounds.right(); x++) {
if (this->containsScratchPoint(x, y)) {
QRgb pixelColor = sampleColor3b(x, y, candidates);

if (colorMatches(rgb, pixelColor))
return true;
}
}
}
return touchingColor(color, false, Value());
}

return false;
bool RenderedTarget::touchingColor(const Value &color, const Value &mask) const
{
return touchingColor(color, true, mask);
}

void RenderedTarget::calculatePos()
Expand Down Expand Up @@ -896,6 +865,70 @@ CpuTextureManager *RenderedTarget::textureManager() const
return m_textureManager.get();
}

bool RenderedTarget::touchingColor(const libscratchcpp::Value &color, bool hasMask, const libscratchcpp::Value &mask) const
{
// https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L775-L841
if (!m_engine)
return false;

QRgb rgb = convertColor(color);
QRgb mask3b;
double ghostValue = 0;

if (hasMask) {
// Ignore ghost effect when checking mask
auto it = m_graphicEffects.find(ShaderManager::Effect::Ghost);

if (it != m_graphicEffects.cend()) {
ghostValue = it->second;
m_graphicEffects.erase(ShaderManager::Effect::Ghost);
}

mask3b = convertColor(mask);
}

std::vector<Target *> targets;
m_engine->getVisibleTargets(targets);

QRectF myRect = touchingBounds();
std::vector<IRenderedTarget *> candidates;
QRectF bounds = candidatesBounds(myRect, targets, candidates);

if (colorMatches(rgb, qRgb(255, 255, 255))) {
// The color we're checking for is the background color which spans the entire stage
bounds = myRect;

if (bounds.isEmpty())
return false;
} else if (candidates.empty()) {
// If not checking for the background color, we can return early if there are no candidate drawables
return false;
}

// Loop through the points of the union
for (int y = bounds.top(); y <= bounds.bottom(); y++) {
for (int x = bounds.left(); x <= bounds.right(); x++) {
if (hasMask ? maskMatches(colorAtScratchPoint(x, y), mask3b) : this->containsScratchPoint(x, y)) {
QRgb pixelColor = sampleColor3b(x, y, candidates);

if (colorMatches(rgb, pixelColor)) {
// Restore ghost effect value
if (hasMask && ghostValue != 0)
m_graphicEffects[ShaderManager::Effect::Ghost] = ghostValue;

return true;
}
}
}
}

// Restore ghost effect value
if (hasMask && ghostValue != 0)
m_graphicEffects[ShaderManager::Effect::Ghost] = ghostValue;

return false;
}

QRectF RenderedTarget::touchingBounds() const
{
// https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L1330-L1350
Expand Down Expand Up @@ -1051,6 +1084,12 @@ QRgb RenderedTarget::convertColor(const libscratchcpp::Value &color)
bool RenderedTarget::colorMatches(QRgb a, QRgb b)
{
// https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L77-L81
return qAlpha(a) > 0 && (qRed(a) & 0b11111000) == (qRed(b) & 0b11111000) && (qGreen(a) & 0b11111000) == (qGreen(b) & 0b11111000) && (qBlue(a) & 0b11110000) == (qBlue(b) & 0b11110000);
}

bool RenderedTarget::maskMatches(QRgb a, QRgb b)
{
// https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L59-L65
return (qRed(a) & 0b11111000) == (qRed(b) & 0b11111000) && (qGreen(a) & 0b11111000) == (qGreen(b) & 0b11111000) && (qBlue(a) & 0b11110000) == (qBlue(b) & 0b11110000);
}

Expand Down
5 changes: 4 additions & 1 deletion src/renderedtarget.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class RenderedTarget : public IRenderedTarget

bool touchingClones(const std::vector<libscratchcpp::Sprite *> &) const override;
bool touchingColor(const libscratchcpp::Value &color) const override;
bool touchingColor(const libscratchcpp::Value &color, const libscratchcpp::Value &mask) const override;

signals:
void engineChanged();
Expand Down Expand Up @@ -131,6 +132,7 @@ class RenderedTarget : public IRenderedTarget
QPointF mapFromStageWithOriginPoint(const QPointF &scenePoint) const;
QPointF mapFromScratchToLocal(const QPointF &point) const;
CpuTextureManager *textureManager() const;
bool touchingColor(const libscratchcpp::Value &color, bool hasMask, const libscratchcpp::Value &mask) const;
QRectF touchingBounds() const;
QRectF candidatesBounds(const QRectF &targetRect, const std::vector<libscratchcpp::Target *> &candidates, std::vector<IRenderedTarget *> &dst) const;
QRectF candidatesBounds(const QRectF &targetRect, const std::vector<libscratchcpp::Sprite *> &candidates, std::vector<IRenderedTarget *> &dst) const;
Expand All @@ -139,6 +141,7 @@ class RenderedTarget : public IRenderedTarget
static void clampRect(libscratchcpp::Rect &rect, double left, double right, double bottom, double top);
static QRgb convertColor(const libscratchcpp::Value &color);
static bool colorMatches(QRgb a, QRgb b);
static bool maskMatches(QRgb a, QRgb b);
QRgb sampleColor3b(double x, double y, const std::vector<IRenderedTarget *> &targets) const;

libscratchcpp::IEngine *m_engine = nullptr;
Expand All @@ -156,7 +159,7 @@ class RenderedTarget : public IRenderedTarget
Texture m_cpuTexture; // without stage scale
mutable std::shared_ptr<CpuTextureManager> m_textureManager; // NOTE: Use textureManager()!
std::unique_ptr<QOpenGLFunctions> m_glF;
std::unordered_map<ShaderManager::Effect, double> m_graphicEffects;
mutable std::unordered_map<ShaderManager::Effect, double> m_graphicEffects;
double m_size = 1;
double m_x = 0;
double m_y = 0;
Expand Down
1 change: 1 addition & 0 deletions test/mocks/renderedtargetmock.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class RenderedTargetMock : public IRenderedTarget

MOCK_METHOD(bool, touchingClones, (const std::vector<libscratchcpp::Sprite *> &), (const, override));
MOCK_METHOD(bool, touchingColor, (const libscratchcpp::Value &), (const, override));
MOCK_METHOD(bool, touchingColor, (const libscratchcpp::Value &, const libscratchcpp::Value &), (const, override));

MOCK_METHOD(QNanoQuickItemPainter *, createItemPainter, (), (const, override));
MOCK_METHOD(void, hoverEnterEvent, (QHoverEvent *), (override));
Expand Down
27 changes: 27 additions & 0 deletions test/renderedtarget/renderedtarget_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,33 @@ TEST_F(RenderedTargetTest, TouchingColor)
EXPECT_CALL(stageTarget, colorAtScratchPoint).Times(0);
ASSERT_FALSE(target.touchingColor(color3));

// Mask (color is touching color)
EXPECT_CALL(stageTarget, getFastBounds()).WillOnce(Return(Rect(2, 1, 6, -5)));
EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2, 1, 6, -5)));
EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -6, 2, -8)));
EXPECT_CALL(target2, colorAtScratchPoint(3, -3)).WillOnce(Return(color4.toInt()));
EXPECT_CALL(target1, colorAtScratchPoint(3, -3)).WillOnce(Return(color1.toInt()));
ASSERT_TRUE(target.touchingColor(color5, color3));

EXPECT_CALL(stageTarget, getFastBounds()).WillOnce(Return(Rect(5, 1, 6, -5)));
EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(5, 1, 6, -5)));
EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -6, 2, -8)));
EXPECT_CALL(target2, colorAtScratchPoint).Times(0);
EXPECT_CALL(target1, colorAtScratchPoint).Times(0);
EXPECT_CALL(penLayer, colorAtScratchPoint).Times(0);
EXPECT_CALL(stageTarget, colorAtScratchPoint).Times(0);
ASSERT_FALSE(target.touchingColor(color3, color3));

// Ghost effect shouldn't affect mask check
target.setGraphicEffect(ShaderManager::Effect::Ghost, 100);
EXPECT_CALL(stageTarget, getFastBounds()).WillOnce(Return(Rect(2, 1, 6, -5)));
EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2, 1, 6, -5)));
EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -6, 2, -8)));
EXPECT_CALL(target2, colorAtScratchPoint(3, -3)).WillOnce(Return(color4.toInt()));
EXPECT_CALL(target1, colorAtScratchPoint(3, -3)).WillOnce(Return(color1.toInt()));
ASSERT_TRUE(target.touchingColor(color5, color3));
ASSERT_EQ(target.graphicEffects().at(ShaderManager::Effect::Ghost), 100);

// Out of bounds: top left
target.updateX(-300);
target.updateY(200);
Expand Down

0 comments on commit 4b62855

Please sign in to comment.