From ef67e3dc51749a8e02eae87fc1792f7e28acdad0 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:15:20 +0100 Subject: [PATCH 01/18] Update libscratchcpp to latest master --- libscratchcpp | 2 +- src/projectloader.cpp | 23 +++++++---------------- src/spritemodel.cpp | 10 ++++++++++ src/spritemodel.h | 3 +++ src/stagemodel.cpp | 20 ++++++++++++++++++++ src/stagemodel.h | 6 ++++++ test/mocks/enginemock.h | 17 +++++++++-------- test/projectloader/projectloader_test.cpp | 11 +++++------ 8 files changed, 61 insertions(+), 31 deletions(-) diff --git a/libscratchcpp b/libscratchcpp index 9fbd24f..9a1d262 160000 --- a/libscratchcpp +++ b/libscratchcpp @@ -1 +1 @@ -Subproject commit 9fbd24f1abc9a225db8e73088a85739fe8a8dfd0 +Subproject commit 9a1d262bfcf778ddb2b24f339e9aa5ed95ba8ad0 diff --git a/src/projectloader.cpp b/src/projectloader.cpp index 84a7544..cfe95be 100644 --- a/src/projectloader.cpp +++ b/src/projectloader.cpp @@ -20,7 +20,7 @@ using namespace libscratchcpp; ProjectLoader::ProjectLoader(QObject *parent) : QObject(parent) { - m_project.setDownloadProgressCallback([this](unsigned int finished, unsigned int all) { + m_project.downloadProgressChanged().connect([this](unsigned int finished, unsigned int all) { if (finished != m_downloadedAssets) { m_downloadedAssets = finished; emit downloadedAssetsChanged(); @@ -208,12 +208,8 @@ void ProjectLoader::stop() void ProjectLoader::answerQuestion(const QString &answer) { - if (m_engine) { - auto f = m_engine->questionAnswered(); - - if (f) - f(answer.toStdString()); - } + if (m_engine) + m_engine->questionAnswered()(answer.toStdString()); } void ProjectLoader::timerEvent(QTimerEvent *event) @@ -255,16 +251,11 @@ void ProjectLoader::load() m_engine->setCloneLimit(m_cloneLimit); m_engine->setSpriteFencingEnabled(m_spriteFencing); - auto redrawHandler = std::bind(&ProjectLoader::redraw, this); - m_engine->setRedrawHandler(std::function(redrawHandler)); - - auto addMonitorHandler = std::bind(&ProjectLoader::addMonitor, this, std::placeholders::_1); - m_engine->setAddMonitorHandler(std::function(addMonitorHandler)); - - auto removeMonitorHandler = std::bind(&ProjectLoader::removeMonitor, this, std::placeholders::_1, std::placeholders::_2); - m_engine->setRemoveMonitorHandler(std::function(removeMonitorHandler)); + m_engine->aboutToRender().connect(&ProjectLoader::redraw, this); + m_engine->monitorAdded().connect(&ProjectLoader::addMonitor, this); + m_engine->monitorRemoved().connect(&ProjectLoader::removeMonitor, this); - m_engine->setQuestionAsked([this](const std::string &question) { emit questionAsked(QString::fromStdString(question)); }); + m_engine->questionAsked().connect([this](const std::string &question) { emit questionAsked(QString::fromStdString(question)); }); // Load targets const auto &targets = m_engine->targets(); diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp index 498838c..8dd654a 100644 --- a/src/spritemodel.cpp +++ b/src/spritemodel.cpp @@ -150,6 +150,16 @@ libscratchcpp::Rect SpriteModel::fastBoundingRect() const return m_renderedTarget->getFastBounds(); } +bool SpriteModel::touchingClones(const std::vector &clones) const +{ + return false; +} + +bool SpriteModel::touchingPoint(double x, double y) const +{ + return false; +} + libscratchcpp::Sprite *SpriteModel::sprite() const { return m_sprite; diff --git a/src/spritemodel.h b/src/spritemodel.h index e6669c5..bc8da74 100644 --- a/src/spritemodel.h +++ b/src/spritemodel.h @@ -57,6 +57,9 @@ class SpriteModel libscratchcpp::Rect boundingRect() const override; libscratchcpp::Rect fastBoundingRect() const override; + bool touchingClones(const std::vector &clones) const override; + bool touchingPoint(double x, double y) const override; + libscratchcpp::Sprite *sprite() const; IRenderedTarget *renderedTarget() const; diff --git a/src/stagemodel.cpp b/src/stagemodel.cpp index 1379c80..ec4e57e 100644 --- a/src/stagemodel.cpp +++ b/src/stagemodel.cpp @@ -77,6 +77,26 @@ void StageModel::onBubbleTextChanged(const std::string &text) } } +libscratchcpp::Rect StageModel::boundingRect() const +{ + return libscratchcpp::Rect(); +} + +libscratchcpp::Rect StageModel::fastBoundingRect() const +{ + return libscratchcpp::Rect(); +} + +bool StageModel::touchingClones(const std::vector &clones) const +{ + return false; +} + +bool StageModel::touchingPoint(double x, double y) const +{ + return false; +} + void StageModel::loadCostume() { if (m_renderedTarget && m_stage) diff --git a/src/stagemodel.h b/src/stagemodel.h index 11e3929..e70ef81 100644 --- a/src/stagemodel.h +++ b/src/stagemodel.h @@ -40,6 +40,12 @@ class StageModel void onBubbleTypeChanged(libscratchcpp::Target::BubbleType type) override; void onBubbleTextChanged(const std::string &text) override; + libscratchcpp::Rect boundingRect() const override; + libscratchcpp::Rect fastBoundingRect() const override; + + bool touchingClones(const std::vector &clones) const override; + bool touchingPoint(double x, double y) const override; + Q_INVOKABLE void loadCostume(); libscratchcpp::Stage *stage() const; diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h index bae97e2..89db942 100644 --- a/test/mocks/enginemock.h +++ b/test/mocks/enginemock.h @@ -34,7 +34,8 @@ class EngineMock : public IEngine MOCK_METHOD(void, runEventLoop, (), (override)); MOCK_METHOD(void, stopEventLoop, (), (override)); - MOCK_METHOD(void, setRedrawHandler, (const std::function &), (override)); + MOCK_METHOD(sigslot::signal<> &, aboutToRender, (), (override)); + MOCK_METHOD(sigslot::signal &, threadAboutToStop, (), (override)); MOCK_METHOD(bool, isRunning, (), (const, override)); @@ -86,8 +87,10 @@ class EngineMock : public IEngine MOCK_METHOD(void, registerSection, (std::shared_ptr), (override)); MOCK_METHOD(unsigned int, functionIndex, (BlockFunc), (override)); + MOCK_METHOD(const std::vector &, blockFunctions, (), (const, override)); MOCK_METHOD(void, addCompileFunction, (IBlockSection *, const std::string &, BlockComp), (override)); + MOCK_METHOD(void, addHatPredicateCompileFunction, (IBlockSection *, const std::string &, HatPredicateCompileFunc), (override)); MOCK_METHOD(void, addMonitorNameFunction, (IBlockSection *, const std::string &, MonitorNameFunc), (override)); MOCK_METHOD(void, addMonitorChangeFunction, (IBlockSection *, const std::string &, MonitorChangeFunc), (override)); MOCK_METHOD(void, addHatBlock, (IBlockSection *, const std::string &), (override)); @@ -107,6 +110,7 @@ class EngineMock : public IEngine MOCK_METHOD(void, addCloneInitScript, (std::shared_ptr), (override)); MOCK_METHOD(void, addKeyPressScript, (std::shared_ptr, int), (override)); MOCK_METHOD(void, addTargetClickScript, (std::shared_ptr), (override)); + MOCK_METHOD(void, addWhenGreaterThanScript, (std::shared_ptr), (override)); MOCK_METHOD(const std::vector> &, targets, (), (const, override)); MOCK_METHOD(void, setTargets, (const std::vector> &), (override)); @@ -123,14 +127,11 @@ class EngineMock : public IEngine MOCK_METHOD(const std::vector> &, monitors, (), (const, override)); MOCK_METHOD(void, setMonitors, (const std::vector> &), (override)); - MOCK_METHOD(void, setAddMonitorHandler, (const std::function &), (override)); - MOCK_METHOD(void, setRemoveMonitorHandler, (const std::function &), (override)); + MOCK_METHOD(sigslot::signal &, monitorAdded, (), (override)); + MOCK_METHOD((sigslot::signal &), monitorRemoved, (), (override)); - MOCK_METHOD(const std::function &, questionAsked, (), (const, override)); - MOCK_METHOD(void, setQuestionAsked, (const std::function &), (override)); - - MOCK_METHOD(const std::function &, questionAnswered, (), (const, override)); - MOCK_METHOD(void, setQuestionAnswered, (const std::function &), (override)); + MOCK_METHOD(sigslot::signal &, questionAsked, (), (override)); + MOCK_METHOD(sigslot::signal &, questionAnswered, (), (override)); MOCK_METHOD(std::vector &, extensions, (), (const, override)); MOCK_METHOD(void, setExtensions, (const std::vector &), (override)); diff --git a/test/projectloader/projectloader_test.cpp b/test/projectloader/projectloader_test.cpp index 64e83da..29ba171 100644 --- a/test/projectloader/projectloader_test.cpp +++ b/test/projectloader/projectloader_test.cpp @@ -96,7 +96,7 @@ TEST_F(ProjectLoaderTest, Load) ASSERT_EQ(sprites[1]->sprite(), engine->targetAt(2)); const auto &monitors = loader.monitorList(); - ASSERT_EQ(monitors.size(), 10); + ASSERT_EQ(monitors.size(), 11); ListMonitorModel *listMonitorModel = dynamic_cast(monitors[0]); ASSERT_EQ(listMonitorModel->monitor(), engine->monitors().at(0).get()); @@ -179,10 +179,8 @@ TEST_F(ProjectLoaderTest, QuestionAsked) load(&loader, "load_test.sb3"); auto engine = loader.engine(); - auto f = engine->questionAsked(); - ASSERT_TRUE(f); ASSERT_TRUE(spy.isEmpty()); - f("test"); + engine->questionAsked()("test"); ASSERT_EQ(spy.count(), 1); auto args = spy.takeFirst(); @@ -197,8 +195,9 @@ TEST_F(ProjectLoaderTest, AnswerQuestion) loader.setEngine(&engine); AnswerQuestionMock mock; - std::function f = std::bind(&AnswerQuestionMock::answer, &mock, std::placeholders::_1); - EXPECT_CALL(engine, questionAnswered()).WillOnce(ReturnRef(f)); + sigslot::signal answered; + answered.connect(&AnswerQuestionMock::answer, &mock); + EXPECT_CALL(engine, questionAnswered()).WillOnce(ReturnRef(answered)); EXPECT_CALL(mock, answer("hello")); loader.answerQuestion("hello"); } From 83e3df6a1e95a8f6171bd0416141e2271cb573db Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 28 Mar 2024 18:27:51 +0100 Subject: [PATCH 02/18] Add CpuTextureManager class --- src/CMakeLists.txt | 2 + src/cputexturemanager.cpp | 118 ++++++++++++++++++++++++ src/cputexturemanager.h | 30 ++++++ test/texture/CMakeLists.txt | 17 ++++ test/texture/cputexturemanager_test.cpp | 107 +++++++++++++++++++++ 5 files changed, 274 insertions(+) create mode 100644 src/cputexturemanager.cpp create mode 100644 src/cputexturemanager.h create mode 100644 test/texture/cputexturemanager_test.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 48d5064..fdf033a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -70,6 +70,8 @@ qt_add_qml_module(scratchcpp-render textbubbleshape.h textbubblepainter.cpp textbubblepainter.h + cputexturemanager.cpp + cputexturemanager.h blocks/penextension.cpp blocks/penextension.h blocks/penblocks.cpp diff --git a/src/cputexturemanager.cpp b/src/cputexturemanager.cpp new file mode 100644 index 0000000..963b0d1 --- /dev/null +++ b/src/cputexturemanager.cpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "cputexturemanager.h" +#include "texture.h" + +using namespace scratchcpprender; + +CpuTextureManager::CpuTextureManager() +{ +} + +CpuTextureManager::~CpuTextureManager() +{ + for (const auto &[handle, data] : m_textureData) + delete[] data; +} + +GLubyte *CpuTextureManager::getTextureData(const Texture &texture) +{ + if (!texture.isValid()) + return nullptr; + + const GLuint handle = texture.handle(); + auto it = m_textureData.find(handle); + + if (it == m_textureData.cend()) { + if (addTexture(texture)) + return m_textureData[handle]; + else + return nullptr; + } else + return it->second; +} + +const std::vector &CpuTextureManager::getTextureConvexHullPoints(const Texture &texture) +{ + static const std::vector empty; + + if (!texture.isValid()) + return empty; + + const GLuint handle = texture.handle(); + auto it = m_convexHullPoints.find(handle); + + if (it == m_convexHullPoints.cend()) { + if (addTexture(texture)) + return m_convexHullPoints[handle]; + else + return empty; + } else + return it->second; +} + +bool CpuTextureManager::addTexture(const Texture &texture) +{ + if (!texture.isValid()) + return false; + + const GLuint handle = texture.handle(); + const int width = texture.width(); + const int height = texture.height(); + + QOpenGLFunctions glF; + glF.initializeOpenGLFunctions(); + + // Create a FBO for the texture + unsigned int fbo; + glF.glGenFramebuffers(1, &fbo); + glF.glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glF.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, handle, 0); + + if (glF.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + qWarning() << "error: framebuffer incomplete (CpuTextureManager)"; + glF.glDeleteFramebuffers(1, &fbo); + return false; + } + + // Read pixels + GLubyte *pixels = new GLubyte[width * height * 4]; // 4 channels (RGBA) + glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + + // Flip vertically + int rowSize = width * 4; + GLubyte *tempRow = new GLubyte[rowSize]; + + for (size_t i = 0; i < height / 2; ++i) { + size_t topRowIndex = i * rowSize; + size_t bottomRowIndex = (height - 1 - i) * rowSize; + + // Swap rows + memcpy(tempRow, &pixels[topRowIndex], rowSize); + memcpy(&pixels[topRowIndex], &pixels[bottomRowIndex], rowSize); + memcpy(&pixels[bottomRowIndex], tempRow, rowSize); + } + + delete[] tempRow; + + m_textureData[handle] = pixels; + m_convexHullPoints[handle] = {}; + std::vector &hullPoints = m_convexHullPoints[handle]; + + // Get convex hull points + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int index = (y * width + x) * 4; // 4 channels (RGBA) + + // Check alpha channel + if (pixels[index + 3] > 0) + hullPoints.push_back(QPoint(x, y)); + } + } + + // Cleanup + glF.glBindFramebuffer(GL_FRAMEBUFFER, 0); + glF.glDeleteFramebuffers(1, &fbo); + + return true; +} diff --git a/src/cputexturemanager.h b/src/cputexturemanager.h new file mode 100644 index 0000000..6b5cace --- /dev/null +++ b/src/cputexturemanager.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include +#include +#include + +namespace scratchcpprender +{ + +class Texture; + +class CpuTextureManager +{ + public: + CpuTextureManager(); + ~CpuTextureManager(); + + GLubyte *getTextureData(const Texture &texture); + const std::vector &getTextureConvexHullPoints(const Texture &texture); + + private: + bool addTexture(const Texture &texture); + + std::unordered_map m_textureData; + std::unordered_map> m_convexHullPoints; +}; + +} // namespace scratchcpprender diff --git a/test/texture/CMakeLists.txt b/test/texture/CMakeLists.txt index 18d74a5..c4c2f04 100644 --- a/test/texture/CMakeLists.txt +++ b/test/texture/CMakeLists.txt @@ -1,3 +1,4 @@ +# texture_test add_executable( texture_test texture_test.cpp @@ -12,3 +13,19 @@ target_link_libraries( add_test(texture_test) gtest_discover_tests(texture_test) + +# cputexturemanager_test +add_executable( + cputexturemanager_test + cputexturemanager_test.cpp +) + +target_link_libraries( + cputexturemanager_test + GTest::gtest_main + scratchcpp-render + ${QT_LIBS} +) + +add_test(cputexturemanager_test) +gtest_discover_tests(cputexturemanager_test) diff --git a/test/texture/cputexturemanager_test.cpp b/test/texture/cputexturemanager_test.cpp new file mode 100644 index 0000000..375f59c --- /dev/null +++ b/test/texture/cputexturemanager_test.cpp @@ -0,0 +1,107 @@ +#include +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; + +class CpuTextureManagerTest : public testing::Test +{ + public: + void createContextAndSurface(QOpenGLContext *context, QOffscreenSurface *surface) + { + QSurfaceFormat surfaceFormat; + surfaceFormat.setMajorVersion(4); + surfaceFormat.setMinorVersion(3); + + context->setFormat(surfaceFormat); + context->create(); + ASSERT_TRUE(context->isValid()); + + surface->setFormat(surfaceFormat); + surface->create(); + ASSERT_TRUE(surface->isValid()); + + context->makeCurrent(surface); + ASSERT_EQ(QOpenGLContext::currentContext(), context); + } +}; + +class ImagePainter +{ + public: + ImagePainter(QNanoPainter *painter, const QString &fileName) + { + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + + // Begin painting + m_fbo = std::make_unique(4, 6, format); + m_fbo->bind(); + painter->beginFrame(m_fbo->width(), m_fbo->height()); + + // Paint + QNanoImage image = QNanoImage::fromCache(painter, fileName); + painter->drawImage(image, 0, 0); + painter->endFrame(); + } + + ~ImagePainter() { m_fbo->release(); } + + QOpenGLFramebufferObject *fbo() const { return m_fbo.get(); }; + + private: + std::unique_ptr m_fbo; +}; + +TEST_F(CpuTextureManagerTest, TextureDataAndHullPoints) +{ + static const GLubyte refData1[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 255, 255, 255, 128, 128, 255, 0, 0, 0, 0, 0, 0, 128, 255, 0, 0, 0, 0, 87, 149, 87, 149, + 0, 0, 0, 0, 128, 0, 128, 255, 128, 128, 255, 255, 128, 128, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + + static const GLubyte refData2[] = { + 0, 0, 57, 255, 10, 0, 50, 255, 43, 0, 35, 255, 60, 0, 28, 255, 0, 0, 55, 255, 39, 15, 73, 255, 137, 85, 133, 255, 207, 142, 182, 255, + 10, 0, 50, 255, 23, 4, 50, 255, 4, 0, 7, 255, 204, 204, 196, 255, 11, 0, 35, 255, 59, 46, 76, 255, 135, 146, 140, 255, 99, 123, 99, 255, + 4, 0, 12, 255, 1, 0, 7, 255, 0, 1, 0, 255, 0, 3, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255 + }; + + static const std::vector refHullPoints1 = { { 1, 1 }, { 2, 1 }, { 3, 1 }, { 1, 2 }, { 3, 2 }, { 1, 3 }, { 2, 3 }, { 3, 3 } }; + + static const std::vector refHullPoints2 = { + { 0, 0 }, { 1, 0 }, { 2, 0 }, { 3, 0 }, { 0, 1 }, { 1, 1 }, { 2, 1 }, { 3, 1 }, { 0, 2 }, { 1, 2 }, { 2, 2 }, { 3, 2 }, + { 0, 3 }, { 1, 3 }, { 2, 3 }, { 3, 3 }, { 0, 4 }, { 1, 4 }, { 2, 4 }, { 3, 4 }, { 0, 5 }, { 1, 5 }, { 2, 5 }, { 3, 5 } + }; + + // Create OpenGL context + QOpenGLContext context; + QOffscreenSurface surface; + createContextAndSurface(&context, &surface); + + // Paint images + QNanoPainter painter; + ImagePainter imgPainter1(&painter, "image.png"); + ImagePainter imgPainter2(&painter, "image.jpg"); + + // Read texture data + CpuTextureManager manager; + + for (int i = 0; i < 2; i++) { + Texture texture1(imgPainter1.fbo()->texture(), imgPainter1.fbo()->size()); + GLubyte *data = manager.getTextureData(texture1); + ASSERT_EQ(memcmp(data, refData1, 96), 0); + const auto &hullPoints1 = manager.getTextureConvexHullPoints(texture1); + ASSERT_EQ(hullPoints1, refHullPoints1); + + Texture texture2(imgPainter2.fbo()->texture(), imgPainter2.fbo()->size()); + data = manager.getTextureData(texture2); + ASSERT_EQ(memcmp(data, refData2, 96), 0); + const auto &hullPoints2 = manager.getTextureConvexHullPoints(texture2); + ASSERT_EQ(hullPoints2, refHullPoints2); + } + + // Cleanup + context.doneCurrent(); +} From e700c788d5f8f29f799f37ae253aaeb286f86e5d Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 28 Mar 2024 18:53:27 +0100 Subject: [PATCH 03/18] Refactor convex hull points --- src/irenderedtarget.h | 3 +- src/renderedtarget.cpp | 106 ++++++++++---------- src/renderedtarget.h | 11 +- src/targetpainter.cpp | 2 +- test/mocks/renderedtargetmock.h | 3 +- test/renderedtarget/renderedtarget_test.cpp | 83 ++------------- test/targetpainter/targetpainter_test.cpp | 2 - 7 files changed, 73 insertions(+), 137 deletions(-) diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h index 5c26352..432b38a 100644 --- a/src/irenderedtarget.h +++ b/src/irenderedtarget.h @@ -83,8 +83,7 @@ class IRenderedTarget : public QNanoQuickItem virtual void setGraphicEffect(ShaderManager::Effect effect, double value) = 0; virtual void clearGraphicEffects() = 0; - virtual void updateHullPoints(QOpenGLFramebufferObject *fbo) = 0; - virtual const std::vector &hullPoints() const = 0; + virtual const std::vector &hullPoints() const = 0; }; } // namespace scratchcpprender diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 20262ce..7228e17 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -13,6 +13,7 @@ #include "scenemousearea.h" #include "bitmapskin.h" #include "svgskin.h" +#include "cputexturemanager.h" using namespace scratchcpprender; using namespace libscratchcpp; @@ -41,6 +42,7 @@ void RenderedTarget::updateVisibility(bool visible) setVisible(visible); calculatePos(); + m_convexHullDirty = true; } void RenderedTarget::updateX(double x) @@ -212,6 +214,7 @@ void RenderedTarget::setEngine(IEngine *newEngine) m_skin = nullptr; m_texture = Texture(); m_oldTexture = Texture(); + m_convexHullDirty = true; clearGraphicEffects(); m_hullPoints.clear(); @@ -256,9 +259,12 @@ void RenderedTarget::setSpriteModel(SpriteModel *newSpriteModel) SpriteModel *cloneRoot = m_spriteModel->cloneRoot(); if (cloneRoot) { - // Inherit skins from the clone root + // Inherit skins, texture mananger, convex hull points, etc. from the clone root RenderedTarget *target = dynamic_cast(cloneRoot->renderedTarget()); Q_ASSERT(target); + m_textureManager = target->m_textureManager; + m_convexHullDirty = target->m_convexHullDirty; + m_hullPoints = target->m_hullPoints; if (target->costumesLoaded()) { m_skins = target->m_skins; // TODO: Avoid copying - maybe using a pointer? @@ -365,7 +371,9 @@ Rect RenderedTarget::getBounds() const double right = -std::numeric_limits::infinity(); double bottom = std::numeric_limits::infinity(); - for (const QPointF &point : m_hullPoints) { + const std::vector &points = hullPoints(); + + for (const QPointF &point : points) { QPointF transformed = transformPoint(point.x() - width / 2, height / 2 - point.y(), originX, originY, rot); const double x = transformed.x() * scale() / m_stageScale * (m_mirrorHorizontally ? -1 : 1); const double y = transformed.y() * scale() / m_stageScale; @@ -516,6 +524,8 @@ void RenderedTarget::setGraphicEffect(ShaderManager::Effect effect, double value if (changed) update(); + + // TODO: Set m_convexHullDirty to true if the effect changes shape } void RenderedTarget::clearGraphicEffects() @@ -523,60 +533,15 @@ void RenderedTarget::clearGraphicEffects() if (!m_graphicEffects.empty()) update(); + // TODO: Set m_convexHullDirty to true if any of the previous effects changed shape m_graphicEffects.clear(); } -void RenderedTarget::updateHullPoints(QOpenGLFramebufferObject *fbo) +const std::vector &RenderedTarget::hullPoints() const { - Q_ASSERT(fbo); - - if (!m_glF) { - m_glF = std::make_unique(); - m_glF->initializeOpenGLFunctions(); - } - - int width = fbo->width(); - int height = fbo->height(); - m_hullPoints.clear(); - m_hullPoints.reserve(width * height); - - // Read pixels from framebuffer - size_t size = width * height * 4; - GLubyte *pixelData = new GLubyte[size]; - m_glF->glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixelData); - - // Flip vertically - int rowSize = width * 4; - GLubyte *tempRow = new GLubyte[rowSize]; - - for (size_t i = 0; i < height / 2; ++i) { - size_t topRowIndex = i * rowSize; - size_t bottomRowIndex = (height - 1 - i) * rowSize; - - // Swap rows - memcpy(tempRow, &pixelData[topRowIndex], rowSize); - memcpy(&pixelData[topRowIndex], &pixelData[bottomRowIndex], rowSize); - memcpy(&pixelData[bottomRowIndex], tempRow, rowSize); - } - - delete[] tempRow; - - // Fill hull points vector - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int index = (y * width + x) * 4; // RGBA channels - - // Check alpha channel - if (pixelData[index + 3] > 0) - m_hullPoints.push_back(QPointF(x, y)); - } - } - - delete[] pixelData; -} + if (convexHullPointsNeeded()) + const_cast(this)->updateHullPoints(); -const std::vector &RenderedTarget::hullPoints() const -{ return m_hullPoints; } @@ -588,12 +553,12 @@ bool RenderedTarget::contains(const QPointF &point) const if (!boundingRect().contains(point)) return false; + const std::vector &points = hullPoints(); + QPoint intPoint = point.toPoint(); - auto it = std::lower_bound(m_hullPoints.begin(), m_hullPoints.end(), intPoint, [](const QPointF &lhs, const QPointF &rhs) { - return (lhs.y() < rhs.y()) || (lhs.y() == rhs.y() && lhs.x() < rhs.x()); - }); + auto it = std::lower_bound(points.begin(), points.end(), intPoint, [](const QPointF &lhs, const QPointF &rhs) { return (lhs.y() < rhs.y()) || (lhs.y() == rhs.y() && lhs.x() < rhs.x()); }); - if (it == m_hullPoints.end()) { + if (it == points.end()) { // The point is beyond the last point in the convex hull return false; } @@ -659,10 +624,15 @@ void RenderedTarget::calculateRotation() void RenderedTarget::calculateSize() { if (m_skin && m_costume) { + GLuint oldTexture = m_texture.handle(); + bool wasValid = m_texture.isValid(); m_texture = m_skin->getTexture(m_size * m_stageScale); m_width = m_texture.width(); m_height = m_texture.height(); setScale(m_size * m_stageScale / m_skin->getTextureScale(m_texture) / m_costume->bitmapResolution()); + + if (wasValid && m_texture.handle() != oldTexture) + m_convexHullDirty = true; } } @@ -679,6 +649,24 @@ void RenderedTarget::handleSceneMouseMove(qreal x, qreal y) } } +bool RenderedTarget::convexHullPointsNeeded() const +{ + return m_convexHullDirty || m_hullPoints.empty(); +} + +void RenderedTarget::updateHullPoints() +{ + m_convexHullDirty = false; + + if (!isVisible()) { + m_hullPoints.clear(); + return; + } + + m_hullPoints = textureManager()->getTextureConvexHullPoints(m_texture); + // TODO: Apply graphic effects (#117) +} + QPointF RenderedTarget::transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const { const double cosRot = std::cos(rot); @@ -688,6 +676,14 @@ QPointF RenderedTarget::transformPoint(double scratchX, double scratchY, double return QPointF(x, y); } +CpuTextureManager *RenderedTarget::textureManager() +{ + if (!m_textureManager) + m_textureManager = std::make_shared(); + + return m_textureManager.get(); +} + bool RenderedTarget::mirrorHorizontally() const { return m_mirrorHorizontally; diff --git a/src/renderedtarget.h b/src/renderedtarget.h index d11f4d2..7d82132 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -19,6 +19,7 @@ namespace scratchcpprender { class Skin; +class CpuTextureManager; class RenderedTarget : public IRenderedTarget { @@ -88,8 +89,7 @@ class RenderedTarget : public IRenderedTarget void setGraphicEffect(ShaderManager::Effect effect, double value) override; void clearGraphicEffects() override; - void updateHullPoints(QOpenGLFramebufferObject *fbo) override; - const std::vector &hullPoints() const override; + const std::vector &hullPoints() const override; Q_INVOKABLE bool contains(const QPointF &point) const override; @@ -112,7 +112,10 @@ class RenderedTarget : public IRenderedTarget void calculateRotation(); void calculateSize(); void handleSceneMouseMove(qreal x, qreal y); + bool convexHullPointsNeeded() const; + void updateHullPoints(); QPointF transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const; + CpuTextureManager *textureManager(); libscratchcpp::IEngine *m_engine = nullptr; libscratchcpp::Costume *m_costume = nullptr; @@ -125,6 +128,7 @@ class RenderedTarget : public IRenderedTarget Skin *m_skin = nullptr; Texture m_texture; Texture m_oldTexture; + std::shared_ptr m_textureManager; // NOTE: Use textureManager()! std::unique_ptr m_glF; std::unordered_map m_graphicEffects; double m_size = 1; @@ -138,7 +142,8 @@ class RenderedTarget : public IRenderedTarget double m_stageScale = 1; qreal m_maximumWidth = std::numeric_limits::infinity(); qreal m_maximumHeight = std::numeric_limits::infinity(); - std::vector m_hullPoints; + bool m_convexHullDirty = true; + std::vector m_hullPoints; bool m_clicked = false; // left mouse button only! double m_dragDeltaX = 0; double m_dragDeltaY = 0; diff --git a/src/targetpainter.cpp b/src/targetpainter.cpp index d25b572..bd1cc40 100644 --- a/src/targetpainter.cpp +++ b/src/targetpainter.cpp @@ -99,7 +99,7 @@ void TargetPainter::paint(QNanoPainter *painter) // Process the resulting texture // NOTE: This must happen now, not later, because the alpha channel can be used here - m_target->updateHullPoints(targetFbo); + // Currently nothing is happening here... // Cleanup shaderProgram->release(); diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h index 695253e..d0faf96 100644 --- a/test/mocks/renderedtargetmock.h +++ b/test/mocks/renderedtargetmock.h @@ -67,8 +67,7 @@ class RenderedTargetMock : public IRenderedTarget MOCK_METHOD(void, setGraphicEffect, (ShaderManager::Effect effect, double value), (override)); MOCK_METHOD(void, clearGraphicEffects, (), (override)); - MOCK_METHOD(void, updateHullPoints, (QOpenGLFramebufferObject *), (override)); - MOCK_METHOD(const std::vector &, hullPoints, (), (const, override)); + MOCK_METHOD(const std::vector &, hullPoints, (), (const, override)); MOCK_METHOD(bool, contains, (const QPointF &), (const, override)); MOCK_METHOD(QNanoQuickItemPainter *, createItemPainter, (), (const, override)); diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index fb02831..2402e31 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -314,27 +314,20 @@ TEST_F(RenderedTargetTest, HullPoints) QOffscreenSurface surface; createContextAndSurface(&context, &surface); - // Create a painter - QNanoPainter painter; - - QOpenGLFramebufferObjectFormat format; - format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); - - // Begin painting - QOpenGLFramebufferObject fbo(4, 6, format); - fbo.bind(); - painter.beginFrame(fbo.width(), fbo.height()); - - // Paint - QNanoImage image = QNanoImage::fromCache(&painter, "image.png"); - painter.drawImage(image, 0, 0); - painter.endFrame(); + // Load costume + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + auto costume = std::make_shared("", "", "png"); + std::string costumeData = readFileStr("image.png"); + costume->setData(costumeData.size(), static_cast(costumeData.data())); + sprite.addCostume(costume); + target.loadCostumes(); + target.updateCostume(costume.get()); // Test hull points target.setWidth(3); target.setHeight(3); - target.updateHullPoints(&fbo); - ASSERT_EQ(target.hullPoints(), std::vector({ { 1, 1 }, { 2, 1 }, { 3, 1 }, { 1, 2 }, { 3, 2 }, { 1, 3 }, { 2, 3 }, { 3, 3 } })); + ASSERT_EQ(target.hullPoints(), std::vector({ { 1, 1 }, { 2, 1 }, { 3, 1 }, { 1, 2 }, { 3, 2 }, { 1, 3 }, { 2, 3 }, { 3, 3 } })); // Test contains() ASSERT_FALSE(target.contains({ 0, 0 })); @@ -358,49 +351,7 @@ TEST_F(RenderedTargetTest, HullPoints) ASSERT_TRUE(target.contains({ 3, 3 })); ASSERT_FALSE(target.contains({ 3.3, 3.5 })); - // Stage: hull points - Stage stage; - StageModel stageModel; - stageModel.init(&stage); - target.setSpriteModel(nullptr); - target.setStageModel(&stageModel); - - target.setWidth(3); - target.setHeight(3); - fbo.release(); - QOpenGLFramebufferObject emptyFbo(fbo.size(), format); - emptyFbo.bind(); - target.updateHullPoints(&emptyFbo); // clear the convex hull points list - ASSERT_TRUE(target.hullPoints().empty()); - emptyFbo.release(); - fbo.bind(); - target.updateHullPoints(&fbo); - ASSERT_EQ(target.hullPoints(), std::vector({ { 1, 1 }, { 2, 1 }, { 3, 1 }, { 1, 2 }, { 3, 2 }, { 1, 3 }, { 2, 3 }, { 3, 3 } })); - - // Stage: contains() - ASSERT_TRUE(target.contains({ 0, 0 })); - ASSERT_TRUE(target.contains({ 1, 0 })); - ASSERT_TRUE(target.contains({ 2, 0 })); - ASSERT_TRUE(target.contains({ 3, 0 })); - - ASSERT_TRUE(target.contains({ 0, 1 })); - ASSERT_TRUE(target.contains({ 1, 1 })); - ASSERT_TRUE(target.contains({ 1.4, 1.25 })); - ASSERT_TRUE(target.contains({ 2, 1 })); - ASSERT_TRUE(target.contains({ 3, 1 })); - - ASSERT_TRUE(target.contains({ 1, 2 })); - ASSERT_TRUE(target.contains({ 2, 2 })); - ASSERT_TRUE(target.contains({ 3, 2 })); - ASSERT_TRUE(target.contains({ 3.5, 2.1 })); - - ASSERT_TRUE(target.contains({ 1, 3 })); - ASSERT_TRUE(target.contains({ 2, 3 })); - ASSERT_TRUE(target.contains({ 3, 3 })); - ASSERT_TRUE(target.contains({ 3.3, 3.5 })); - - // Release - fbo.release(); + // Cleanup context.doneCurrent(); } @@ -640,8 +591,6 @@ TEST_F(RenderedTargetTest, GetBounds) QOpenGLContext context; QOffscreenSurface surface; createContextAndSurface(&context, &surface); - QOpenGLExtraFunctions glF(&context); - glF.initializeOpenGLFunctions(); RenderedTarget target; Sprite sprite; @@ -668,16 +617,6 @@ TEST_F(RenderedTargetTest, GetBounds) target.updateCostume(costume.get()); target.beforeRedraw(); - Texture texture = target.texture(); - QOpenGLFramebufferObjectFormat format; - format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); - - QOpenGLFramebufferObject fbo(texture.size(), format); - fbo.bind(); - glF.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.handle(), 0); - target.updateHullPoints(&fbo); - fbo.release(); - Rect bounds = target.getBounds(); ASSERT_EQ(std::round(bounds.left() * 100) / 100, 66.13); ASSERT_EQ(std::round(bounds.top() * 100) / 100, -124.52); diff --git a/test/targetpainter/targetpainter_test.cpp b/test/targetpainter/targetpainter_test.cpp index 1d4c7b3..57175be 100644 --- a/test/targetpainter/targetpainter_test.cpp +++ b/test/targetpainter/targetpainter_test.cpp @@ -82,7 +82,6 @@ TEST_F(TargetPainterTest, Paint) std::unordered_map effects; EXPECT_CALL(target, texture()).WillOnce(Return(texture)); EXPECT_CALL(target, graphicEffects()).WillOnce(ReturnRef(effects)); - EXPECT_CALL(target, updateHullPoints(&fbo)); targetPainter.paint(&painter); painter.endFrame(); @@ -97,7 +96,6 @@ TEST_F(TargetPainterTest, Paint) effects[ShaderManager::Effect::Ghost] = 84; EXPECT_CALL(target, texture()).WillOnce(Return(texture)); EXPECT_CALL(target, graphicEffects()).WillOnce(ReturnRef(effects)); - EXPECT_CALL(target, updateHullPoints(&fbo)); targetPainter.paint(&painter); painter.endFrame(); effects.clear(); From a2e9a1feb56aa2c9169e05bc53a61e505221f9d3 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 28 Mar 2024 18:59:47 +0100 Subject: [PATCH 04/18] Change RenderedTarget parent type to QQuickItem --- src/irenderedtarget.h | 2 +- src/renderedtarget.cpp | 2 +- src/renderedtarget.h | 2 +- test/renderedtarget/renderedtarget_test.cpp | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h index 432b38a..8b7a316 100644 --- a/src/irenderedtarget.h +++ b/src/irenderedtarget.h @@ -23,7 +23,7 @@ class Texture; class IRenderedTarget : public QNanoQuickItem { public: - IRenderedTarget(QNanoQuickItem *parent = nullptr) : + IRenderedTarget(QQuickItem *parent = nullptr) : QNanoQuickItem(parent) { } diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 7228e17..9c5070d 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -21,7 +21,7 @@ using namespace libscratchcpp; static const double SVG_SCALE_LIMIT = 0.1; // the maximum viewport dimensions are multiplied by this static const double pi = std::acos(-1); // TODO: Use std::numbers::pi in C++20 -RenderedTarget::RenderedTarget(QNanoQuickItem *parent) : +RenderedTarget::RenderedTarget(QQuickItem *parent) : IRenderedTarget(parent) { setSmooth(false); diff --git a/src/renderedtarget.h b/src/renderedtarget.h index 7d82132..68e9dd7 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -33,7 +33,7 @@ class RenderedTarget : public IRenderedTarget Q_PROPERTY(double stageScale READ stageScale WRITE setStageScale NOTIFY stageScaleChanged) public: - RenderedTarget(QNanoQuickItem *parent = nullptr); + RenderedTarget(QQuickItem *parent = nullptr); ~RenderedTarget(); void updateVisibility(bool visible) override; diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index 2402e31..5b3b002 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -43,10 +43,10 @@ class RenderedTargetTest : public testing::Test TEST_F(RenderedTargetTest, Constructors) { - RenderedTarget target1; - RenderedTarget target2(&target1); - ASSERT_EQ(target2.parent(), &target1); - ASSERT_EQ(target2.parentItem(), &target1); + QQuickItem item1; + QQuickItem item2(&item1); + ASSERT_EQ(item2.parent(), &item1); + ASSERT_EQ(item2.parentItem(), &item1); } TEST_F(RenderedTargetTest, UpdateMethods) From df8565855dd43e3747fe834377a519991e39d201 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Thu, 28 Mar 2024 22:49:35 +0100 Subject: [PATCH 05/18] Add containsScratchPoint() method --- src/irenderedtarget.h | 2 ++ src/renderedtarget.cpp | 14 +++++++++ src/renderedtarget.h | 1 + test/mocks/renderedtargetmock.h | 2 ++ test/renderedtarget/renderedtarget_test.cpp | 34 +++++++++++++++++++-- 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h index 8b7a316..e836b75 100644 --- a/src/irenderedtarget.h +++ b/src/irenderedtarget.h @@ -84,6 +84,8 @@ class IRenderedTarget : public QNanoQuickItem virtual void clearGraphicEffects() = 0; virtual const std::vector &hullPoints() const = 0; + + virtual bool containsScratchPoint(double x, double y) const = 0; }; } // namespace scratchcpprender diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 9c5070d..8ba681c 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -567,6 +567,20 @@ bool RenderedTarget::contains(const QPointF &point) const return *it == intPoint; } +bool RenderedTarget::containsScratchPoint(double x, double y) const +{ + if (!m_engine || !parentItem()) + return false; + + // contains() expects item coordinates, so translate the Scratch coordinates first + double stageWidth = m_engine->stageWidth(); + double stageHeight = m_engine->stageHeight(); + x = m_stageScale * (x + stageWidth / 2); + y = m_stageScale * (stageHeight / 2 - y); + + return contains(mapFromItem(parentItem(), QPointF(x, y))); +} + void RenderedTarget::calculatePos() { if (!m_skin || !m_costume || !m_engine) diff --git a/src/renderedtarget.h b/src/renderedtarget.h index 68e9dd7..6ac26d8 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -92,6 +92,7 @@ class RenderedTarget : public IRenderedTarget const std::vector &hullPoints() const override; Q_INVOKABLE bool contains(const QPointF &point) const override; + bool containsScratchPoint(double x, double y) const override; signals: void engineChanged(); diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h index d0faf96..1ba83f6 100644 --- a/test/mocks/renderedtargetmock.h +++ b/test/mocks/renderedtargetmock.h @@ -70,6 +70,8 @@ class RenderedTargetMock : public IRenderedTarget MOCK_METHOD(const std::vector &, hullPoints, (), (const, override)); MOCK_METHOD(bool, contains, (const QPointF &), (const, override)); + MOCK_METHOD(bool, containsScratchPoint, (double, double), (const, override)); + MOCK_METHOD(QNanoQuickItemPainter *, createItemPainter, (), (const, override)); MOCK_METHOD(void, hoverEnterEvent, (QHoverEvent *), (override)); MOCK_METHOD(void, hoverLeaveEvent, (QHoverEvent *), (override)); diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index 5b3b002..612145c 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -305,7 +305,10 @@ TEST_F(RenderedTargetTest, HullPoints) SpriteModel model; model.init(&sprite); - RenderedTarget target; + QQuickItem parent; + parent.setWidth(200); + parent.setHeight(100); + RenderedTarget target(&parent); target.setEngine(&engine); target.setSpriteModel(&model); @@ -315,14 +318,17 @@ TEST_F(RenderedTargetTest, HullPoints) createContextAndSurface(&context, &surface); // Load costume - EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); - EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + EXPECT_CALL(engine, stageWidth()).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillRepeatedly(Return(360)); auto costume = std::make_shared("", "", "png"); std::string costumeData = readFileStr("image.png"); costume->setData(costumeData.size(), static_cast(costumeData.data())); sprite.addCostume(costume); target.loadCostumes(); target.updateCostume(costume.get()); + target.setStageScale(2); + target.setX(25); + target.setY(30); // Test hull points target.setWidth(3); @@ -351,6 +357,28 @@ TEST_F(RenderedTargetTest, HullPoints) ASSERT_TRUE(target.contains({ 3, 3 })); ASSERT_FALSE(target.contains({ 3.3, 3.5 })); + // Test containsScratchPoint() + ASSERT_FALSE(target.containsScratchPoint(-227.5, 165)); // [0, 0] + ASSERT_FALSE(target.containsScratchPoint(-226.5, 165)); // [1, 0] + ASSERT_FALSE(target.containsScratchPoint(-225.5, 165)); // [2, 0] + ASSERT_FALSE(target.containsScratchPoint(-224.5, 165)); // [3, 0] + + ASSERT_FALSE(target.containsScratchPoint(-227.5, 164)); // [0, 1] + ASSERT_TRUE(target.containsScratchPoint(-226.5, 164)); // [1, 1] + ASSERT_TRUE(target.containsScratchPoint(-226.1, 163.75)); // [1.4, 1.25] + ASSERT_TRUE(target.containsScratchPoint(-225.5, 164)); // [2, 1] + ASSERT_TRUE(target.containsScratchPoint(-224.5, 164)); // [3, 1] + + ASSERT_TRUE(target.containsScratchPoint(-226.5, 163)); // [1, 2] + ASSERT_FALSE(target.containsScratchPoint(-225.5, 163)); // [2, 2] + ASSERT_TRUE(target.containsScratchPoint(-224.5, 163)); // [3, 2] + ASSERT_FALSE(target.containsScratchPoint(-224, 162.9)); // [3.5, 2.1] + + ASSERT_TRUE(target.containsScratchPoint(-226.5, 162)); // [1, 3] + ASSERT_TRUE(target.containsScratchPoint(-225.5, 162)); // [2, 3] + ASSERT_TRUE(target.containsScratchPoint(-224.5, 162)); // [3, 3] + ASSERT_FALSE(target.containsScratchPoint(-224.2, 161.5)); // [3.3, 3.5] + // Cleanup context.doneCurrent(); } From 9aa85b9645927c3f4a15aed63e3bc86a597ddda6 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 29 Mar 2024 12:08:01 +0100 Subject: [PATCH 06/18] Do not use scaled texture for convex hull points --- src/renderedtarget.cpp | 16 +++++++++++----- src/renderedtarget.h | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 8ba681c..0117a66 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -214,6 +214,7 @@ void RenderedTarget::setEngine(IEngine *newEngine) m_skin = nullptr; m_texture = Texture(); m_oldTexture = Texture(); + m_cpuTexture = Texture(); m_convexHullDirty = true; clearGraphicEffects(); m_hullPoints.clear(); @@ -547,6 +548,9 @@ const std::vector &RenderedTarget::hullPoints() const bool RenderedTarget::contains(const QPointF &point) const { + if (!m_costume || !m_texture.isValid() || !m_cpuTexture.isValid()) + return false; + if (m_stageModel) return true; // the stage contains any point within the scene @@ -555,7 +559,8 @@ bool RenderedTarget::contains(const QPointF &point) const const std::vector &points = hullPoints(); - QPoint intPoint = point.toPoint(); + const double scaleRatio = m_skin->getTextureScale(m_texture) / m_skin->getTextureScale(m_cpuTexture); + QPoint intPoint = (point / scaleRatio).toPoint(); auto it = std::lower_bound(points.begin(), points.end(), intPoint, [](const QPointF &lhs, const QPointF &rhs) { return (lhs.y() < rhs.y()) || (lhs.y() == rhs.y() && lhs.x() < rhs.x()); }); if (it == points.end()) { @@ -638,14 +643,15 @@ void RenderedTarget::calculateRotation() void RenderedTarget::calculateSize() { if (m_skin && m_costume) { - GLuint oldTexture = m_texture.handle(); - bool wasValid = m_texture.isValid(); + GLuint oldTexture = m_cpuTexture.handle(); + bool wasValid = m_cpuTexture.isValid(); m_texture = m_skin->getTexture(m_size * m_stageScale); + m_cpuTexture = m_skin->getTexture(m_size); m_width = m_texture.width(); m_height = m_texture.height(); setScale(m_size * m_stageScale / m_skin->getTextureScale(m_texture) / m_costume->bitmapResolution()); - if (wasValid && m_texture.handle() != oldTexture) + if (wasValid && m_cpuTexture.handle() != oldTexture) m_convexHullDirty = true; } } @@ -677,7 +683,7 @@ void RenderedTarget::updateHullPoints() return; } - m_hullPoints = textureManager()->getTextureConvexHullPoints(m_texture); + m_hullPoints = textureManager()->getTextureConvexHullPoints(m_cpuTexture); // TODO: Apply graphic effects (#117) } diff --git a/src/renderedtarget.h b/src/renderedtarget.h index 6ac26d8..9a36b56 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -129,6 +129,7 @@ class RenderedTarget : public IRenderedTarget Skin *m_skin = nullptr; Texture m_texture; Texture m_oldTexture; + Texture m_cpuTexture; // without stage scale std::shared_ptr m_textureManager; // NOTE: Use textureManager()! std::unique_ptr m_glF; std::unordered_map m_graphicEffects; From 351b27c19f9c395c42177dd12d13bcd125863332 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 29 Mar 2024 19:42:59 +0100 Subject: [PATCH 07/18] fix #119: Translate coordinates in contains() correctly --- src/renderedtarget.cpp | 30 +++++++++++++++++---- src/renderedtarget.h | 1 + test/renderedtarget/renderedtarget_test.cpp | 20 ++++++++++++-- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 0117a66..4efb607 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -548,19 +548,22 @@ const std::vector &RenderedTarget::hullPoints() const bool RenderedTarget::contains(const QPointF &point) const { - if (!m_costume || !m_texture.isValid() || !m_cpuTexture.isValid()) + if (!m_costume || !m_texture.isValid() || !m_cpuTexture.isValid() || !parentItem()) return false; if (m_stageModel) return true; // the stage contains any point within the scene - if (!boundingRect().contains(point)) + const double scaleRatio = m_skin->getTextureScale(m_texture) / m_skin->getTextureScale(m_cpuTexture); + QPointF translatedPoint = mapToItem(parentItem(), point); + translatedPoint = mapFromStageWithOriginPoint(translatedPoint); + translatedPoint /= scaleRatio; + + if (!boundingRect().contains(translatedPoint)) return false; const std::vector &points = hullPoints(); - - const double scaleRatio = m_skin->getTextureScale(m_texture) / m_skin->getTextureScale(m_cpuTexture); - QPoint intPoint = (point / scaleRatio).toPoint(); + QPoint intPoint = translatedPoint.toPoint(); auto it = std::lower_bound(points.begin(), points.end(), intPoint, [](const QPointF &lhs, const QPointF &rhs) { return (lhs.y() < rhs.y()) || (lhs.y() == rhs.y() && lhs.x() < rhs.x()); }); if (it == points.end()) { @@ -696,6 +699,23 @@ QPointF RenderedTarget::transformPoint(double scratchX, double scratchY, double return QPointF(x, y); } +QPointF RenderedTarget::mapFromStageWithOriginPoint(const QPointF &scenePoint) const +{ + // mapFromItem() doesn't use the transformOriginPoint property, so we must do this ourselves + QTransform t; + const double mirror = m_mirrorHorizontally ? -1 : 1; + const double originX = transformOriginPoint().x(); + const double originY = transformOriginPoint().y(); + t.translate(originX, originY); + t.rotate(-rotation()); + t.scale(1 / scale() * mirror, 1 / scale()); + t.translate(-originX * mirror, -originY); + t.translate(-x(), -y()); + + QPointF localPoint = t.map(scenePoint); + return localPoint; +} + CpuTextureManager *RenderedTarget::textureManager() { if (!m_textureManager) diff --git a/src/renderedtarget.h b/src/renderedtarget.h index 9a36b56..b2879b9 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -116,6 +116,7 @@ class RenderedTarget : public IRenderedTarget bool convexHullPointsNeeded() const; void updateHullPoints(); QPointF transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const; + QPointF mapFromStageWithOriginPoint(const QPointF &scenePoint) const; CpuTextureManager *textureManager(); libscratchcpp::IEngine *m_engine = nullptr; diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index 612145c..1956ac4 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -306,8 +306,8 @@ TEST_F(RenderedTargetTest, HullPoints) model.init(&sprite); QQuickItem parent; - parent.setWidth(200); - parent.setHeight(100); + parent.setWidth(480); + parent.setHeight(360); RenderedTarget target(&parent); target.setEngine(&engine); target.setSpriteModel(&model); @@ -357,7 +357,23 @@ TEST_F(RenderedTargetTest, HullPoints) ASSERT_TRUE(target.contains({ 3, 3 })); ASSERT_FALSE(target.contains({ 3.3, 3.5 })); + // Test contains() with horizontal mirroring + target.updateRotationStyle(Sprite::RotationStyle::LeftRight); + target.updateDirection(-45); + target.setX(25); + target.setY(30); + ASSERT_FALSE(target.contains({ 0, 0 })); + ASSERT_TRUE(target.contains({ -1, 1 })); + ASSERT_FALSE(target.contains({ -2, 2 })); + ASSERT_TRUE(target.contains({ -3, 2 })); + ASSERT_FALSE(target.contains({ -3.5, 2.1 })); + ASSERT_TRUE(target.contains({ -2, 3 })); + ASSERT_FALSE(target.contains({ -3.3, 3.5 })); + // Test containsScratchPoint() + target.updateDirection(0); + target.setX(25); + target.setY(30); ASSERT_FALSE(target.containsScratchPoint(-227.5, 165)); // [0, 0] ASSERT_FALSE(target.containsScratchPoint(-226.5, 165)); // [1, 0] ASSERT_FALSE(target.containsScratchPoint(-225.5, 165)); // [2, 0] From 24642277527ba99e6ceb0ae26ac3f955f161d6ce Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:05:52 +0100 Subject: [PATCH 08/18] Implement touching point methods --- src/spritemodel.cpp | 2 +- src/stagemodel.cpp | 2 +- test/target_models/spritemodel_test.cpp | 14 ++++++++++++++ test/target_models/stagemodel_test.cpp | 16 ++++++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp index 8dd654a..144a39b 100644 --- a/src/spritemodel.cpp +++ b/src/spritemodel.cpp @@ -157,7 +157,7 @@ bool SpriteModel::touchingClones(const std::vector &clo bool SpriteModel::touchingPoint(double x, double y) const { - return false; + return m_renderedTarget->containsScratchPoint(x, y); } libscratchcpp::Sprite *SpriteModel::sprite() const diff --git a/src/stagemodel.cpp b/src/stagemodel.cpp index ec4e57e..954159a 100644 --- a/src/stagemodel.cpp +++ b/src/stagemodel.cpp @@ -94,7 +94,7 @@ bool StageModel::touchingClones(const std::vector &clon bool StageModel::touchingPoint(double x, double y) const { - return false; + return m_renderedTarget->containsScratchPoint(x, y); } void StageModel::loadCostume() diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp index 7d3409b..1ed0c95 100644 --- a/test/target_models/spritemodel_test.cpp +++ b/test/target_models/spritemodel_test.cpp @@ -318,6 +318,20 @@ TEST(SpriteModelTest, FastBoundingRect) ASSERT_EQ(bounds.bottom(), rect.bottom()); } +TEST(SpriteModelTest, TouchingPoint) +{ + SpriteModel model; + + RenderedTargetMock renderedTarget; + model.setRenderedTarget(&renderedTarget); + + EXPECT_CALL(renderedTarget, containsScratchPoint(56.3, -179.4)).WillOnce(Return(false)); + ASSERT_FALSE(model.touchingPoint(56.3, -179.4)); + + EXPECT_CALL(renderedTarget, containsScratchPoint(-20.08, 109.47)).WillOnce(Return(true)); + ASSERT_TRUE(model.touchingPoint(-20.08, 109.47)); +} + TEST(SpriteModelTest, RenderedTarget) { SpriteModel model; diff --git a/test/target_models/stagemodel_test.cpp b/test/target_models/stagemodel_test.cpp index 66a50cc..2d0a3c8 100644 --- a/test/target_models/stagemodel_test.cpp +++ b/test/target_models/stagemodel_test.cpp @@ -9,6 +9,8 @@ using namespace scratchcpprender; using namespace libscratchcpp; +using ::testing::Return; + TEST(StageModelTest, Constructors) { StageModel model1; @@ -106,6 +108,20 @@ TEST(StageModelTest, OnBubbleTextChanged) ASSERT_EQ(spy.count(), 2); } +TEST(StageModelTest, TouchingPoint) +{ + StageModel model; + + RenderedTargetMock renderedTarget; + model.setRenderedTarget(&renderedTarget); + + EXPECT_CALL(renderedTarget, containsScratchPoint(56.3, -179.4)).WillOnce(Return(false)); + ASSERT_FALSE(model.touchingPoint(56.3, -179.4)); + + EXPECT_CALL(renderedTarget, containsScratchPoint(-20.08, 109.47)).WillOnce(Return(true)); + ASSERT_TRUE(model.touchingPoint(-20.08, 109.47)); +} + TEST(StageModelTest, RenderedTarget) { StageModel model; From a8c0cbe05396e4215e68759a1069024bc2fbefc3 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 29 Mar 2024 20:49:13 +0100 Subject: [PATCH 09/18] Enable contains() for stage --- src/renderedtarget.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 4efb607..15c0264 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -551,9 +551,6 @@ bool RenderedTarget::contains(const QPointF &point) const if (!m_costume || !m_texture.isValid() || !m_cpuTexture.isValid() || !parentItem()) return false; - if (m_stageModel) - return true; // the stage contains any point within the scene - const double scaleRatio = m_skin->getTextureScale(m_texture) / m_skin->getTextureScale(m_cpuTexture); QPointF translatedPoint = mapToItem(parentItem(), point); translatedPoint = mapFromStageWithOriginPoint(translatedPoint); From 62212231be03135b8bf55bdd8c25889cb51e94d2 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 30 Mar 2024 09:41:25 +0100 Subject: [PATCH 10/18] Use CPU texture in getBounds() --- src/renderedtarget.cpp | 14 ++--- test/renderedtarget/renderedtarget_test.cpp | 64 ++++++++++----------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 15c0264..ed82a7a 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -359,13 +359,13 @@ void RenderedTarget::setHeight(qreal height) Rect RenderedTarget::getBounds() const { // https://github.com/scratchfoundation/scratch-render/blob/c3ede9c3d54769730c7b023021511e2aba167b1f/src/Rectangle.js#L33-L55 - if (!m_costume || !m_skin || !m_texture.isValid()) + if (!m_costume || !m_skin || !m_texture.isValid() || !m_cpuTexture.isValid()) return Rect(m_x, m_y, m_x, m_y); - const double width = m_texture.width() * m_size / scale() / m_costume->bitmapResolution(); - const double height = m_texture.height() * m_size / scale() / m_costume->bitmapResolution(); - const double originX = m_stageScale * m_costume->rotationCenterX() * m_size / scale() / m_costume->bitmapResolution() - width / 2; - const double originY = m_stageScale * -m_costume->rotationCenterY() * m_size / scale() / m_costume->bitmapResolution() + height / 2; + const double width = m_cpuTexture.width() * m_size / m_costume->bitmapResolution(); + const double height = m_cpuTexture.height() * m_size / m_costume->bitmapResolution(); + const double originX = m_costume->rotationCenterX() * m_size / m_costume->bitmapResolution() - width / 2; + const double originY = -m_costume->rotationCenterY() * m_size / m_costume->bitmapResolution() + height / 2; const double rot = -rotation() * pi / 180; double left = std::numeric_limits::infinity(); double top = -std::numeric_limits::infinity(); @@ -376,8 +376,8 @@ Rect RenderedTarget::getBounds() const for (const QPointF &point : points) { QPointF transformed = transformPoint(point.x() - width / 2, height / 2 - point.y(), originX, originY, rot); - const double x = transformed.x() * scale() / m_stageScale * (m_mirrorHorizontally ? -1 : 1); - const double y = transformed.y() * scale() / m_stageScale; + const double x = transformed.x() * (m_mirrorHorizontally ? -1 : 1); + const double y = transformed.y(); if (x < left) left = x; diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index 1956ac4..196a691 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -662,64 +662,64 @@ TEST_F(RenderedTargetTest, GetBounds) target.beforeRedraw(); Rect bounds = target.getBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, 66.13); - ASSERT_EQ(std::round(bounds.top() * 100) / 100, -124.52); - ASSERT_EQ(std::round(bounds.right() * 100) / 100, 66.72); - ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -125.11); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 64.96); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, -121.16); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, 67.79); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -123.99); QRectF bubbleBounds = target.getBoundsForBubble(); - ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, 66.13); - ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, -124.52); - ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, 66.72); - ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, -125.11); + ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, 64.96); + ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, -121.16); + ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, 67.79); + ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, -123.99); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.updateRotationStyle(Sprite::RotationStyle::LeftRight); bounds = target.getBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, 71.87); - ASSERT_EQ(std::round(bounds.top() * 100) / 100, -110.47); - ASSERT_EQ(std::round(bounds.right() * 100) / 100, 72.29); - ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -110.89); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 69.5); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, -111.26); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, 71.5); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -113.26); bubbleBounds = target.getBoundsForBubble(); - ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, 71.87); - ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, -110.47); - ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, 72.29); - ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, -110.89); + ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, 69.5); + ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, -111.26); + ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, 71.5); + ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, -113.26); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.setStageScale(20.75); bounds = target.getBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, 71.87); - ASSERT_EQ(std::round(bounds.top() * 100) / 100, -110.47); - ASSERT_EQ(std::round(bounds.right() * 100) / 100, 72.29); - ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -110.89); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 69.5); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, -111.26); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, 71.5); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -113.26); bubbleBounds = target.getBoundsForBubble(); - ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, 71.87); - ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, -110.47); - ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, 72.29); - ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, -110.89); + ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, 69.5); + ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, -111.26); + ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, 71.5); + ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, -113.26); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.updateSize(9780.6); bounds = target.getBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, -466.05); - ASSERT_EQ(std::round(bounds.top() * 100) / 100, 1294.13); - ASSERT_EQ(std::round(bounds.right() * 100) / 100, -405.87); - ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, 1233.94); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, -378.77); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, 1323.22); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, -376.77); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, 1321.22); bubbleBounds = target.getBoundsForBubble(); - ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, -466.05); - ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, 1294.13); - ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, -405.87); - ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, 1286.13); + ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, -378.77); + ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, 1323.22); + ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, -376.77); + ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, 1321.22); context.doneCurrent(); } From 70e82e2e338d3ba3a9955c4467a20396a0037963 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 30 Mar 2024 09:57:29 +0100 Subject: [PATCH 11/18] Use CPU texture in getFastBounds() --- src/renderedtarget.cpp | 14 +++++++------- test/renderedtarget/renderedtarget_test.cpp | 18 +++++++++--------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index ed82a7a..d4c542a 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -410,14 +410,14 @@ QRectF RenderedTarget::getBoundsForBubble() const Rect RenderedTarget::getFastBounds() const { - if (!m_costume || !m_skin || !m_texture.isValid()) + if (!m_costume || !m_skin || !m_texture.isValid() || !m_cpuTexture.isValid()) return Rect(m_x, m_y, m_x, m_y); - const double scale = this->scale(); - const double width = this->width() / m_stageScale; - const double height = this->height() / m_stageScale; - const double originX = m_costume->rotationCenterX() * m_size / scale / m_costume->bitmapResolution() - width / 2; - const double originY = -m_costume->rotationCenterY() * m_size / scale / m_costume->bitmapResolution() + height / 2; + const double textureScale = m_skin->getTextureScale(m_cpuTexture); + const double width = m_cpuTexture.width() * m_size / textureScale; + const double height = m_cpuTexture.height() * m_size / textureScale; + const double originX = m_costume->rotationCenterX() * m_size / textureScale / m_costume->bitmapResolution() - width / 2; + const double originY = -m_costume->rotationCenterY() * m_size / textureScale / m_costume->bitmapResolution() + height / 2; const double rot = -rotation() * pi / 180; QPointF topLeft = transformPoint(-width / 2, height / 2, originX, originY, rot); @@ -438,7 +438,7 @@ Rect RenderedTarget::getFastBounds() const const double minY = std::min(yList); const double maxY = std::max(yList); - return Rect(minX * scale + m_x, maxY * scale + m_y, maxX * scale + m_x, minY * scale + m_y); + return Rect(minX + m_x, maxY + m_y, maxX + m_x, minY + m_y); } QPointF RenderedTarget::mapFromScene(const QPointF &point) const diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index 196a691..48f4189 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -758,9 +758,9 @@ TEST_F(RenderedTargetTest, GetFastBounds) target.beforeRedraw(); Rect bounds = target.getFastBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, 65.84); - ASSERT_EQ(std::round(bounds.top() * 100) / 100, -123.92); - ASSERT_EQ(std::round(bounds.right() * 100) / 100, 67.31); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 64.47); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, -120.57); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, 69.26); ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -125.4); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); @@ -768,30 +768,30 @@ TEST_F(RenderedTargetTest, GetFastBounds) target.updateRotationStyle(Sprite::RotationStyle::LeftRight); bounds = target.getFastBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, 71.67); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 69.78); ASSERT_EQ(std::round(bounds.top() * 100) / 100, -110.26); ASSERT_EQ(std::round(bounds.right() * 100) / 100, 72.5); - ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -111.51); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -114.34); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.setStageScale(20.75); bounds = target.getFastBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, 71.67); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 69.78); ASSERT_EQ(std::round(bounds.top() * 100) / 100, -110.26); ASSERT_EQ(std::round(bounds.right() * 100) / 100, 72.5); - ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -111.51); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -114.34); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.updateSize(9780.6); bounds = target.getFastBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, -496.15); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, -767); ASSERT_EQ(std::round(bounds.top() * 100) / 100, 1324.22); ASSERT_EQ(std::round(bounds.right() * 100) / 100, -375.77); - ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, 1143.65); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, 737.38); context.doneCurrent(); } From 54f7ed3943eb238790151e7c88efa24bf487da69 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 30 Mar 2024 10:06:46 +0100 Subject: [PATCH 12/18] Add debug code for bounding boxes --- src/ProjectPlayer.qml | 46 ++++++++++++++++++++++++++++++++++++++++++ src/renderedtarget.cpp | 6 ++++++ src/renderedtarget.h | 1 + 3 files changed, 53 insertions(+) diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml index 4e168d9..8e0798f 100644 --- a/src/ProjectPlayer.qml +++ b/src/ProjectPlayer.qml @@ -156,6 +156,52 @@ ProjectScene { } } + // Uncomment to display sprite bounding boxes (for debugging) + /*Rectangle { + function translateX(x) { + // Translates Scratch X-coordinate to the scene coordinate system + return root.stageScale * (root.stageWidth / 2 + x) + } + + function translateY(y) { + // Translates Scratch Y-coordinate to the scene coordinate system + return root.stageScale * (root.stageHeight / 2 - y) + } + + id: boundRect + color: "transparent" + border.color: "red" + border.width: 3 + + function updatePosition() { + let bounds = targetItem.getQmlBounds(); + boundRect.x = translateX(bounds.left); + boundRect.y = translateY(bounds.top); + width = bounds.width * root.stageScale; + height = -bounds.height * root.stageScale; + } + + Connections { + target: targetItem + + function onXChanged() { boundRect.updatePosition() } + function onYChanged() { boundRect.updatePosition() } + function onRotationChanged() { boundRect.updatePosition() } + function onWidthChanged() { boundRect.updatePosition() } + function onHeightChanged() { boundRect.updatePosition() } + function onScaleChanged() { boundRect.updatePosition() } + } + + Connections { + property Scale transform: Scale {} + target: transform + + function onXScaleChanged() { boundRect.updatePosition() } + + Component.onCompleted: transform = targetItem.transform[0] + } + }*/ + Loader { readonly property alias model: targetItem.spriteModel active: model ? model.bubbleText !== "" : false diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index d4c542a..aa89fa8 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -395,6 +395,12 @@ Rect RenderedTarget::getBounds() const return Rect(left + m_x, top + m_y, right + m_x, bottom + m_y); } +QRectF scratchcpprender::RenderedTarget::getQmlBounds() const +{ + Rect bounds = getBounds(); + return QRectF(QPointF(bounds.left(), bounds.top()), QPointF(bounds.right(), bounds.bottom())); +} + QRectF RenderedTarget::getBoundsForBubble() const { // https://github.com/scratchfoundation/scratch-render/blob/86dcb0151a04bc8c1ff39559e8531e7921102b56/src/Drawable.js#L536-L551 diff --git a/src/renderedtarget.h b/src/renderedtarget.h index b2879b9..f1a1be8 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -76,6 +76,7 @@ class RenderedTarget : public IRenderedTarget void setHeight(qreal height) override; libscratchcpp::Rect getBounds() const override; + Q_INVOKABLE QRectF getQmlBounds() const; Q_INVOKABLE QRectF getBoundsForBubble() const override; libscratchcpp::Rect getFastBounds() const override; From 51661db55e3ff3844d43fb3923282b197d37b13d Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 30 Mar 2024 10:07:11 +0100 Subject: [PATCH 13/18] TextBubble: Listen to X scale changes --- src/internal/TextBubble.qml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/internal/TextBubble.qml b/src/internal/TextBubble.qml index 8a77084..f70df26 100644 --- a/src/internal/TextBubble.qml +++ b/src/internal/TextBubble.qml @@ -85,6 +85,15 @@ TextBubbleShape { function onScaleChanged() { positionBubble() } } + Connections { + property Scale transform: Scale {} + target: transform + + function onXScaleChanged() { positionBubble() } + + Component.onCompleted: transform = root.target.transform[0] + } + Text { id: bubbleText anchors.left: parent.left From a20b0f86961d462b4cef577f92bba439f25083fc Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:10:17 +0100 Subject: [PATCH 14/18] Fix transform origin in getFastBounds() --- src/renderedtarget.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index aa89fa8..6fb22f0 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -422,8 +422,8 @@ Rect RenderedTarget::getFastBounds() const const double textureScale = m_skin->getTextureScale(m_cpuTexture); const double width = m_cpuTexture.width() * m_size / textureScale; const double height = m_cpuTexture.height() * m_size / textureScale; - const double originX = m_costume->rotationCenterX() * m_size / textureScale / m_costume->bitmapResolution() - width / 2; - const double originY = -m_costume->rotationCenterY() * m_size / textureScale / m_costume->bitmapResolution() + height / 2; + const double originX = m_costume->rotationCenterX() * m_size / m_costume->bitmapResolution() - width / 2; + const double originY = -m_costume->rotationCenterY() * m_size / m_costume->bitmapResolution() + height / 2; const double rot = -rotation() * pi / 180; QPointF topLeft = transformPoint(-width / 2, height / 2, originX, originY, rot); From 4425f65752d32583101d2e941cb6439b77988288 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:10:55 +0100 Subject: [PATCH 15/18] Add touchingClones() method to RenderedTarget --- src/irenderedtarget.h | 2 + src/renderedtarget.cpp | 52 ++++++++++ src/renderedtarget.h | 2 + test/mocks/renderedtargetmock.h | 2 + test/renderedtarget/renderedtarget_test.cpp | 106 ++++++++++++++++++++ 5 files changed, 164 insertions(+) diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h index e836b75..1a5f267 100644 --- a/src/irenderedtarget.h +++ b/src/irenderedtarget.h @@ -86,6 +86,8 @@ class IRenderedTarget : public QNanoQuickItem virtual const std::vector &hullPoints() const = 0; virtual bool containsScratchPoint(double x, double y) const = 0; + + virtual bool touchingClones(const std::vector &clones) const = 0; }; } // namespace scratchcpprender diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 6fb22f0..09c023e 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -592,6 +592,58 @@ bool RenderedTarget::containsScratchPoint(double x, double y) const return contains(mapFromItem(parentItem(), QPointF(x, y))); } +bool RenderedTarget::touchingClones(const std::vector &clones) const +{ + // https://github.com/scratchfoundation/scratch-render/blob/941562438fe3dd6e7d98d9387607d535dcd68d24/src/RenderWebGL.js#L967-L1002 + // TODO: Use Rect methods and do not use QRects + Rect scratchRect = getFastBounds(); + const QRectF myRect(QPointF(scratchRect.left(), scratchRect.bottom()), QPointF(scratchRect.right(), scratchRect.top())); + QRectF united; + std::vector candidates; + + // Calculate the union of the bounding rectangle intersections + for (auto clone : clones) { + Q_ASSERT(clone); + + if (!clone) + continue; + + SpriteModel *model = static_cast(clone->getInterface()); + Q_ASSERT(model); + + if (model) { + // Calculate the intersection of the bounding rectangles + IRenderedTarget *candidate = model->renderedTarget(); + Q_ASSERT(candidate); + scratchRect = candidate->getFastBounds(); + QRectF rect(QPointF(scratchRect.left(), scratchRect.bottom()), QPointF(scratchRect.right(), scratchRect.top())); + QRectF intersected = myRect.intersected(rect); + + // Add it to the union + united = united.united(intersected); + + candidates.push_back(candidate); + } + } + + if (united.isEmpty() || candidates.empty()) + return false; + + // Loop through the points of the union + for (int y = united.top(); y <= united.bottom(); y++) { + for (int x = united.left(); x <= united.right(); x++) { + if (this->containsScratchPoint(x, y)) { + for (IRenderedTarget *candidate : candidates) { + if (candidate->containsScratchPoint(x, y)) + return true; + } + } + } + } + + return false; +} + void RenderedTarget::calculatePos() { if (!m_skin || !m_costume || !m_engine) diff --git a/src/renderedtarget.h b/src/renderedtarget.h index f1a1be8..3c93f97 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -95,6 +95,8 @@ class RenderedTarget : public IRenderedTarget Q_INVOKABLE bool contains(const QPointF &point) const override; bool containsScratchPoint(double x, double y) const override; + bool touchingClones(const std::vector &) const override; + signals: void engineChanged(); void stageModelChanged(); diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h index 1ba83f6..ee2974e 100644 --- a/test/mocks/renderedtargetmock.h +++ b/test/mocks/renderedtargetmock.h @@ -72,6 +72,8 @@ class RenderedTargetMock : public IRenderedTarget MOCK_METHOD(bool, contains, (const QPointF &), (const, override)); MOCK_METHOD(bool, containsScratchPoint, (double, double), (const, override)); + MOCK_METHOD(bool, touchingClones, (const std::vector &), (const, override)); + MOCK_METHOD(QNanoQuickItemPainter *, createItemPainter, (), (const, override)); MOCK_METHOD(void, hoverEnterEvent, (QHoverEvent *), (override)); MOCK_METHOD(void, hoverLeaveEvent, (QHoverEvent *), (override)); diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index 48f4189..a24edab 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "../common.h" @@ -795,3 +796,108 @@ TEST_F(RenderedTargetTest, GetFastBounds) context.doneCurrent(); } + +TEST_F(RenderedTargetTest, TouchingClones) +{ + EngineMock engine; + Sprite sprite, clone1, clone2; + SpriteModel model, model1, model2; + model.init(&sprite); + clone1.setInterface(&model1); + clone2.setInterface(&model2); + + QQuickItem parent; + parent.setWidth(480); + parent.setHeight(360); + + RenderedTarget target(&parent); + target.setEngine(&engine); + target.setSpriteModel(&model); + + RenderedTargetMock target1, target2; + model1.setRenderedTarget(&target1); + model2.setRenderedTarget(&target2); + + // Create OpenGL context + QOpenGLContext context; + QOffscreenSurface surface; + createContextAndSurface(&context, &surface); + + // Load costume + EXPECT_CALL(engine, stageWidth()).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillRepeatedly(Return(360)); + auto costume = std::make_shared("", "", "png"); + std::string costumeData = readFileStr("image.png"); + costume->setData(costumeData.size(), static_cast(costumeData.data())); + sprite.addCostume(costume); + target.loadCostumes(); + target.updateCostume(costume.get()); + target.setWidth(3); + target.setHeight(3); + + EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2, 1, 6, -5))); + EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -1, 1.8, -8))); + EXPECT_CALL(target1, containsScratchPoint(1, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(2, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(2, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(3, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(3, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(1, -2)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -2)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(3, -2)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(3, -2)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(1, -1)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -1)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(2, -1)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(2, -1)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(3, -1)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(3, -1)).WillOnce(Return(false)); + ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 })); + + EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2, 1, 6, -5))); + EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -1, 1.8, -8))); + EXPECT_CALL(target1, containsScratchPoint(1, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(2, -3)).WillOnce(Return(true)); + ASSERT_TRUE(target.touchingClones({ &clone1, &clone2 })); + + EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(5, 1, 6, -5))); + EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -1, 1.8, -8))); + EXPECT_CALL(target1, containsScratchPoint(1, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(1, -2)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -2)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(1, -1)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -1)).WillOnce(Return(false)); + ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 })); + + EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2, 1, 6, -5))); + EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -6.5, 1.8, -8))); + EXPECT_CALL(target1, containsScratchPoint(2, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(2, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(3, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(3, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(3, -2)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(3, -2)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(2, -1)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(2, -1)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(3, -1)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(3, -1)).WillOnce(Return(false)); + ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 })); + + EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2, 1, 6, -5))); + EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -6.5, 1.8, -8))); + EXPECT_CALL(target1, containsScratchPoint(2, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(2, -3)).WillOnce(Return(true)); + ASSERT_TRUE(target.touchingClones({ &clone1, &clone2 })); + + EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(5, 1, 6, -5))); + EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -6.5, 1.8, -8))); + EXPECT_CALL(target1, containsScratchPoint).Times(0); + EXPECT_CALL(target2, containsScratchPoint).Times(0); + ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 })); + + // Cleanup + context.doneCurrent(); +} From 195c5317831c943452f776962d0d305f669798dc Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:11:56 +0100 Subject: [PATCH 16/18] ProjectPlayer: Hide debug bounding rects if sprite is invisible --- src/ProjectPlayer.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml index 8e0798f..79bf270 100644 --- a/src/ProjectPlayer.qml +++ b/src/ProjectPlayer.qml @@ -172,6 +172,7 @@ ProjectScene { color: "transparent" border.color: "red" border.width: 3 + visible: targetItem.visible function updatePosition() { let bounds = targetItem.getQmlBounds(); From fe37a9b0c419f9fc3d8c731ce6cfecd132901aeb Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:18:00 +0100 Subject: [PATCH 17/18] Implement touching clones methods --- src/spritemodel.cpp | 2 +- src/stagemodel.cpp | 2 +- test/target_models/spritemodel_test.cpp | 26 ++++++++++++++++++++++ test/target_models/stagemodel_test.cpp | 29 +++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp index 144a39b..73eee92 100644 --- a/src/spritemodel.cpp +++ b/src/spritemodel.cpp @@ -152,7 +152,7 @@ libscratchcpp::Rect SpriteModel::fastBoundingRect() const bool SpriteModel::touchingClones(const std::vector &clones) const { - return false; + return m_renderedTarget->touchingClones(clones); } bool SpriteModel::touchingPoint(double x, double y) const diff --git a/src/stagemodel.cpp b/src/stagemodel.cpp index 954159a..d57a0af 100644 --- a/src/stagemodel.cpp +++ b/src/stagemodel.cpp @@ -89,7 +89,7 @@ libscratchcpp::Rect StageModel::fastBoundingRect() const bool StageModel::touchingClones(const std::vector &clones) const { - return false; + return m_renderedTarget->touchingClones(clones); } bool StageModel::touchingPoint(double x, double y) const diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp index 1ed0c95..6f44f88 100644 --- a/test/target_models/spritemodel_test.cpp +++ b/test/target_models/spritemodel_test.cpp @@ -318,6 +318,32 @@ TEST(SpriteModelTest, FastBoundingRect) ASSERT_EQ(bounds.bottom(), rect.bottom()); } +TEST(SpriteModelTest, TouchingClones) +{ + SpriteModel model; + + RenderedTargetMock renderedTarget; + model.setRenderedTarget(&renderedTarget); + + Sprite clone1, clone2; + std::vector clones = { &clone1, &clone2 }; + std::vector actualClones; + + EXPECT_CALL(renderedTarget, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&actualClones](const std::vector &candidates) { + actualClones = candidates; + return false; + }))); + ASSERT_FALSE(model.touchingClones(clones)); + ASSERT_EQ(actualClones, clones); + + EXPECT_CALL(renderedTarget, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&actualClones](const std::vector &candidates) { + actualClones = candidates; + return true; + }))); + ASSERT_TRUE(model.touchingClones(clones)); + ASSERT_EQ(actualClones, clones); +} + TEST(SpriteModelTest, TouchingPoint) { SpriteModel model; diff --git a/test/target_models/stagemodel_test.cpp b/test/target_models/stagemodel_test.cpp index 2d0a3c8..2c4bf69 100644 --- a/test/target_models/stagemodel_test.cpp +++ b/test/target_models/stagemodel_test.cpp @@ -10,6 +10,9 @@ using namespace scratchcpprender; using namespace libscratchcpp; using ::testing::Return; +using ::testing::WithArgs; +using ::testing::Invoke; +using ::testing::_; TEST(StageModelTest, Constructors) { @@ -108,6 +111,32 @@ TEST(StageModelTest, OnBubbleTextChanged) ASSERT_EQ(spy.count(), 2); } +TEST(SpriteModelTest, TouchingClones) +{ + StageModel model; + + RenderedTargetMock renderedTarget; + model.setRenderedTarget(&renderedTarget); + + Sprite clone1, clone2; + std::vector clones = { &clone1, &clone2 }; + std::vector actualClones; + + EXPECT_CALL(renderedTarget, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&actualClones](const std::vector &candidates) { + actualClones = candidates; + return false; + }))); + ASSERT_FALSE(model.touchingClones(clones)); + ASSERT_EQ(actualClones, clones); + + EXPECT_CALL(renderedTarget, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&actualClones](const std::vector &candidates) { + actualClones = candidates; + return true; + }))); + ASSERT_TRUE(model.touchingClones(clones)); + ASSERT_EQ(actualClones, clones); +} + TEST(StageModelTest, TouchingPoint) { StageModel model; From a017e1a2f01d82863ec58d14c2008d951186af7b Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:22:10 +0100 Subject: [PATCH 18/18] RenderedTarget: Optimize point transformation --- src/renderedtarget.cpp | 21 ++++++++++++++------- src/renderedtarget.h | 1 + 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 09c023e..6c33896 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -367,6 +367,8 @@ Rect RenderedTarget::getBounds() const const double originX = m_costume->rotationCenterX() * m_size / m_costume->bitmapResolution() - width / 2; const double originY = -m_costume->rotationCenterY() * m_size / m_costume->bitmapResolution() + height / 2; const double rot = -rotation() * pi / 180; + const double sinRot = std::sin(rot); + const double cosRot = std::cos(rot); double left = std::numeric_limits::infinity(); double top = -std::numeric_limits::infinity(); double right = -std::numeric_limits::infinity(); @@ -375,7 +377,7 @@ Rect RenderedTarget::getBounds() const const std::vector &points = hullPoints(); for (const QPointF &point : points) { - QPointF transformed = transformPoint(point.x() - width / 2, height / 2 - point.y(), originX, originY, rot); + QPointF transformed = transformPoint(point.x() - width / 2, height / 2 - point.y(), originX, originY, sinRot, cosRot); const double x = transformed.x() * (m_mirrorHorizontally ? -1 : 1); const double y = transformed.y(); @@ -425,11 +427,13 @@ Rect RenderedTarget::getFastBounds() const const double originX = m_costume->rotationCenterX() * m_size / m_costume->bitmapResolution() - width / 2; const double originY = -m_costume->rotationCenterY() * m_size / m_costume->bitmapResolution() + height / 2; const double rot = -rotation() * pi / 180; + const double sinRot = std::sin(rot); + const double cosRot = std::cos(rot); - QPointF topLeft = transformPoint(-width / 2, height / 2, originX, originY, rot); - QPointF topRight = transformPoint(width / 2, height / 2, originX, originY, rot); - QPointF bottomRight = transformPoint(width / 2, -height / 2, originX, originY, rot); - QPointF bottomLeft = transformPoint(-width / 2, -height / 2, originX, originY, rot); + QPointF topLeft = transformPoint(-width / 2, height / 2, originX, originY, sinRot, cosRot); + QPointF topRight = transformPoint(width / 2, height / 2, originX, originY, sinRot, cosRot); + QPointF bottomRight = transformPoint(width / 2, -height / 2, originX, originY, sinRot, cosRot); + QPointF bottomLeft = transformPoint(-width / 2, -height / 2, originX, originY, sinRot, cosRot); if (m_mirrorHorizontally) { topLeft.setX(-topLeft.x()); @@ -747,8 +751,11 @@ void RenderedTarget::updateHullPoints() QPointF RenderedTarget::transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const { - const double cosRot = std::cos(rot); - const double sinRot = std::sin(rot); + return transformPoint(scratchX, scratchY, originX, originY, std::sin(rot), std::cos(rot)); +} + +QPointF RenderedTarget::transformPoint(double scratchX, double scratchY, double originX, double originY, double sinRot, double cosRot) const +{ const double x = (scratchX - originX) * cosRot - (scratchY - originY) * sinRot; const double y = (scratchX - originX) * sinRot + (scratchY - originY) * cosRot; return QPointF(x, y); diff --git a/src/renderedtarget.h b/src/renderedtarget.h index 3c93f97..3b3d0c5 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -119,6 +119,7 @@ class RenderedTarget : public IRenderedTarget bool convexHullPointsNeeded() const; void updateHullPoints(); QPointF transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const; + QPointF transformPoint(double scratchX, double scratchY, double originX, double originY, double sinRot, double cosRot) const; QPointF mapFromStageWithOriginPoint(const QPointF &scenePoint) const; CpuTextureManager *textureManager();