From 0ed4900fd78679c938b9bcf39eeeaf4273103c30 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 18 Feb 2024 11:52:11 +0100
Subject: [PATCH 01/70] RenderedTarget: Optimize calculateSize()

---
 src/renderedtarget.cpp | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 9ddb101..dcb6805 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -602,11 +602,10 @@ void RenderedTarget::calculateRotation()
 void RenderedTarget::calculateSize()
 {
     if (m_skin && m_costume) {
-        Texture texture = m_skin->getTexture(m_size * m_stageScale);
-        m_texture = texture;
-        m_width = texture.width();
-        m_height = texture.height();
-        setScale(m_size * m_stageScale / m_skin->getTextureScale(texture) / m_costume->bitmapResolution());
+        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());
     }
 }
 

From 86a2cf118117db4727d1b603504d981238ceab79 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 18 Feb 2024 11:52:59 +0100
Subject: [PATCH 02/70] fix #95: Update target dimensions after changing stage
 scale

---
 src/renderedtarget.cpp                      | 1 +
 test/renderedtarget/renderedtarget_test.cpp | 7 +++++++
 2 files changed, 8 insertions(+)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index dcb6805..2cf7854 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -307,6 +307,7 @@ void RenderedTarget::setStageScale(double newStageScale)
     m_stageScale = newStageScale;
     calculateSize();
     calculatePos();
+    beforeRedraw();
     emit stageScaleChanged();
 }
 
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index ba79e3b..0ff9acc 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -270,6 +270,13 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     ASSERT_EQ(texture.width(), 26);
     ASSERT_EQ(texture.height(), 26);
 
+    // Stage scale (SVG) - should update width and height
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.setStageScale(3.5);
+    ASSERT_EQ(target.width(), 52);
+    ASSERT_EQ(target.height(), 52);
+
     context.doneCurrent();
 }
 

From 485cea84c78618ca1e7850d677bbc5edc006fdfb Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 18 Feb 2024 12:20:06 +0100
Subject: [PATCH 03/70] fix #97: Enable smooth sampling for SVG costumes

---
 src/renderedtarget.cpp                      | 2 ++
 test/renderedtarget/renderedtarget_test.cpp | 3 +++
 2 files changed, 5 insertions(+)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 9ddb101..0dab0f5 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -114,6 +114,8 @@ void RenderedTarget::updateCostume(Costume *costume)
             m_skin = it->second;
     }
 
+    setSmooth(m_costume->dataFormat() == "svg");
+
     calculateSize();
     calculatePos();
 }
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index ba79e3b..125f800 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -89,6 +89,7 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     ASSERT_EQ(target.transformOriginPoint(), QPointF(-23, 72));
     ASSERT_EQ(target.transformOrigin(), QQuickItem::Center);
     ASSERT_EQ(target.scale(), 0.4);
+    ASSERT_FALSE(target.smooth());
 
     target.setStageModel(nullptr);
     ASSERT_TRUE(mirrorHorizontallySpy.empty());
@@ -128,6 +129,7 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     ASSERT_EQ(target.transformOrigin(), QQuickItem::Center);
     ASSERT_EQ(std::round(target.scale() * 100) / 100, 0.58);
     ASSERT_TRUE(mirrorHorizontallySpy.empty());
+    ASSERT_FALSE(target.smooth());
 
     texture = target.texture();
     ASSERT_TRUE(texture.isValid());
@@ -264,6 +266,7 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     ASSERT_EQ(target.transformOriginPoint().y(), -16);
     ASSERT_EQ(target.transformOrigin(), QQuickItem::Center);
     ASSERT_EQ(std::round(target.scale() * 100) / 100, 0.75);
+    ASSERT_TRUE(target.smooth());
 
     texture = target.texture();
     ASSERT_TRUE(texture.isValid());

From a5bd59ab63848462b276919de9171a5bc3713dd1 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 18 Feb 2024 12:28:52 +0100
Subject: [PATCH 04/70] Update libscratchcpp to v0.8.1

---
 libscratchcpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libscratchcpp b/libscratchcpp
index 4f51860..16ab889 160000
--- a/libscratchcpp
+++ b/libscratchcpp
@@ -1 +1 @@
-Subproject commit 4f51860662928084cddd053ad1fed8bce274f80f
+Subproject commit 16ab889b11c5ab70b4da19eb9b6e1a76afec10e0

From e12906debcaf4de065f139fe4974064ac7c01f67 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Wed, 21 Feb 2024 20:58:46 +0100
Subject: [PATCH 05/70] Update unit test CMake option in test workflow

---
 .github/workflows/utests.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/utests.yml b/.github/workflows/utests.yml
index d900a94..b4db17f 100644
--- a/.github/workflows/utests.yml
+++ b/.github/workflows/utests.yml
@@ -28,7 +28,7 @@ jobs:
       with:
         version: '6.6.*'
     - name: Configure CMake
-      run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DSCRATCHCPPGUI_BUILD_UNIT_TESTS=ON
+      run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DSCRATCHCPPRENDER_BUILD_UNIT_TESTS=ON
 
     - name: Build
       run: xvfb-run cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} -j$(nproc --all)

From affa08fae51ada6702157d88f6296b0f111b0738 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 24 Feb 2024 16:00:42 +0100
Subject: [PATCH 06/70] Update libscratchcpp to latest master

---
 libscratchcpp       | 2 +-
 src/spritemodel.cpp | 5 +++++
 src/spritemodel.h   | 1 +
 3 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/libscratchcpp b/libscratchcpp
index 16ab889..2e3d3df 160000
--- a/libscratchcpp
+++ b/libscratchcpp
@@ -1 +1 @@
-Subproject commit 16ab889b11c5ab70b4da19eb9b6e1a76afec10e0
+Subproject commit 2e3d3df69476937958a83a01747c0c7c38a48f42
diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp
index 706bed7..997f083 100644
--- a/src/spritemodel.cpp
+++ b/src/spritemodel.cpp
@@ -145,6 +145,11 @@ libscratchcpp::Rect SpriteModel::boundingRect() const
     return m_renderedTarget->getBounds();
 }
 
+libscratchcpp::Rect SpriteModel::fastBoundingRect() const
+{
+    return libscratchcpp::Rect(m_sprite->x(), m_sprite->y(), m_sprite->x(), m_sprite->y());
+}
+
 libscratchcpp::Sprite *SpriteModel::sprite() const
 {
     return m_sprite;
diff --git a/src/spritemodel.h b/src/spritemodel.h
index cb4903e..e6669c5 100644
--- a/src/spritemodel.h
+++ b/src/spritemodel.h
@@ -55,6 +55,7 @@ class SpriteModel
         void onBubbleTextChanged(const std::string &text) override;
 
         libscratchcpp::Rect boundingRect() const override;
+        libscratchcpp::Rect fastBoundingRect() const override;
 
         libscratchcpp::Sprite *sprite() const;
 

From 1601a0c4ba27e5b9687fb4066e2d10f6ae931356 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 24 Feb 2024 16:37:13 +0100
Subject: [PATCH 07/70] Add getFastBounds() method to RenderedTarget

---
 src/irenderedtarget.h                       |  1 +
 src/renderedtarget.cpp                      | 33 ++++++++++
 src/renderedtarget.h                        |  1 +
 test/mocks/renderedtargetmock.h             |  1 +
 test/renderedtarget/renderedtarget_test.cpp | 72 +++++++++++++++++++++
 5 files changed, 108 insertions(+)

diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h
index 6d70c57..5c26352 100644
--- a/src/irenderedtarget.h
+++ b/src/irenderedtarget.h
@@ -71,6 +71,7 @@ class IRenderedTarget : public QNanoQuickItem
 
         virtual libscratchcpp::Rect getBounds() const = 0;
         virtual QRectF getBoundsForBubble() const = 0;
+        virtual libscratchcpp::Rect getFastBounds() const = 0;
 
         virtual QPointF mapFromScene(const QPointF &point) const = 0;
 
diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 7467265..067e420 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -383,6 +383,39 @@ QRectF RenderedTarget::getBoundsForBubble() const
     return QRectF(QPointF(rect.left(), rect.top()), QPointF(rect.right(), rect.bottom()));
 }
 
+Rect RenderedTarget::getFastBounds() const
+{
+    if (!m_costume || !m_skin || !m_texture.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 rot = -rotation() * pi / 180;
+
+    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);
+
+    if (m_mirrorHorizontally) {
+        topLeft.setX(-topLeft.x());
+        topRight.setX(-topRight.x());
+        bottomRight.setX(-bottomRight.x());
+        bottomLeft.setX(-bottomLeft.x());
+    }
+    const auto xList = { topLeft.x(), topRight.x(), bottomRight.x(), bottomLeft.x() };
+    const auto yList = { topLeft.y(), topRight.y(), bottomRight.y(), bottomLeft.y() };
+    const double minX = std::min(xList);
+    const double maxX = std::max(xList);
+    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);
+}
+
 QPointF RenderedTarget::mapFromScene(const QPointF &point) const
 {
     return QNanoQuickItem::mapFromScene(point);
diff --git a/src/renderedtarget.h b/src/renderedtarget.h
index 7d80182..75ed7af 100644
--- a/src/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -76,6 +76,7 @@ class RenderedTarget : public IRenderedTarget
 
         libscratchcpp::Rect getBounds() const override;
         Q_INVOKABLE QRectF getBoundsForBubble() const override;
+        libscratchcpp::Rect getFastBounds() const override;
 
         QPointF mapFromScene(const QPointF &point) const override;
 
diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h
index aa31376..695253e 100644
--- a/test/mocks/renderedtargetmock.h
+++ b/test/mocks/renderedtargetmock.h
@@ -56,6 +56,7 @@ class RenderedTargetMock : public IRenderedTarget
         MOCK_METHOD(QPointF, mapFromScene, (const QPointF &), (const, override));
 
         MOCK_METHOD(libscratchcpp::Rect, getBounds, (), (const, override));
+        MOCK_METHOD(libscratchcpp::Rect, getFastBounds, (), (const, override));
         MOCK_METHOD(QRectF, getBoundsForBubble, (), (const, override));
 
         MOCK_METHOD(bool, mirrorHorizontally, (), (const, override));
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index 5c2dd07..b595ba8 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -740,3 +740,75 @@ TEST_F(RenderedTargetTest, GetBounds)
 
     context.doneCurrent();
 }
+
+TEST_F(RenderedTargetTest, GetFastBounds)
+{
+    QOpenGLContext context;
+    QOffscreenSurface surface;
+    createContextAndSurface(&context, &surface);
+    QOpenGLExtraFunctions glF(&context);
+    glF.initializeOpenGLFunctions();
+    RenderedTarget target;
+
+    Sprite sprite;
+    sprite.setX(75.64);
+    sprite.setY(-120.3);
+    sprite.setDirection(-46.37);
+    sprite.setSize(67.98);
+    SpriteModel spriteModel;
+    sprite.setInterface(&spriteModel);
+    target.setSpriteModel(&spriteModel);
+    EngineMock engine;
+    target.setEngine(&engine);
+    auto costume = std::make_shared<Costume>("", "", "png");
+    std::string costumeData = readFileStr("image.png");
+    costume->setData(costumeData.size(), static_cast<void *>(costumeData.data()));
+    costume->setRotationCenterX(-15);
+    costume->setRotationCenterY(48);
+    costume->setBitmapResolution(3.25);
+    sprite.addCostume(costume);
+
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.loadCostumes();
+    target.updateCostume(costume.get());
+    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.bottom() * 100) / 100, -125.4);
+
+    EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
+    EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
+    target.updateRotationStyle(Sprite::RotationStyle::LeftRight);
+
+    bounds = target.getFastBounds();
+    ASSERT_EQ(std::round(bounds.left() * 100) / 100, 71.67);
+    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);
+
+    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.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);
+
+    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.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);
+
+    context.doneCurrent();
+}

From c698762457f43faa2af6127bbdfd1ddaff749d36 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 24 Feb 2024 16:41:06 +0100
Subject: [PATCH 08/70] Implement SpriteModel::fastBoundingRect()

---
 src/spritemodel.cpp                     |  2 +-
 test/target_models/spritemodel_test.cpp | 16 ++++++++++++++++
 2 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp
index 997f083..498838c 100644
--- a/src/spritemodel.cpp
+++ b/src/spritemodel.cpp
@@ -147,7 +147,7 @@ libscratchcpp::Rect SpriteModel::boundingRect() const
 
 libscratchcpp::Rect SpriteModel::fastBoundingRect() const
 {
-    return libscratchcpp::Rect(m_sprite->x(), m_sprite->y(), m_sprite->x(), m_sprite->y());
+    return m_renderedTarget->getFastBounds();
 }
 
 libscratchcpp::Sprite *SpriteModel::sprite() const
diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp
index b2eca9e..7d3409b 100644
--- a/test/target_models/spritemodel_test.cpp
+++ b/test/target_models/spritemodel_test.cpp
@@ -302,6 +302,22 @@ TEST(SpriteModelTest, BoundingRect)
     ASSERT_EQ(bounds.bottom(), rect.bottom());
 }
 
+TEST(SpriteModelTest, FastBoundingRect)
+{
+    SpriteModel model;
+
+    RenderedTargetMock renderedTarget;
+    model.setRenderedTarget(&renderedTarget);
+
+    Rect rect(-1, 1, 1, -1);
+    EXPECT_CALL(renderedTarget, getFastBounds()).WillOnce(Return(rect));
+    Rect bounds = model.fastBoundingRect();
+    ASSERT_EQ(bounds.left(), rect.left());
+    ASSERT_EQ(bounds.top(), rect.top());
+    ASSERT_EQ(bounds.right(), rect.right());
+    ASSERT_EQ(bounds.bottom(), rect.bottom());
+}
+
 TEST(SpriteModelTest, RenderedTarget)
 {
     SpriteModel model;

From a5ab6f8da885cabdb2e36d835639a93307b728e0 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 24 Feb 2024 20:31:55 +0100
Subject: [PATCH 09/70] README: Mark question text box as finished

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 9b194fe..36f35b9 100644
--- a/README.md
+++ b/README.md
@@ -146,7 +146,7 @@ qputenv("QSG_RENDER_LOOP", "basic");
 - [x] Monitors
 - [ ] Graphics effects (color, brightness and ghost are implemented)
 - [x] Speech and thought bubbles
-- [ ] Question text box ("ask and wait" block)
+- [x] Question text box ("ask and wait" block)
 
 See the [open issues](https://github.com/scratchcpp/scratchcpp-render/issues) for a full list of proposed features (and known issues).
 

From 445b5428fc6510d4b9ee015546331f133df202a3 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 25 Feb 2024 12:06:06 +0100
Subject: [PATCH 10/70] Add global init function

---
 CMakeLists.txt                                  |  7 +++++++
 README.md                                       | 12 ++++++++----
 include/scratchcpp-render/scratchcpp-render.h   | 12 ++++++++++++
 src/CMakeLists.txt                              | 11 ++++++-----
 src/global_functions.cpp                        | 10 ++++++++++
 src/penlayerpainter.cpp                         |  6 ++----
 src/targetpainter.cpp                           |  6 ++----
 test/CMakeLists.txt                             |  1 +
 test/global_functions/CMakeLists.txt            | 15 +++++++++++++++
 test/global_functions/global_functions_test.cpp | 14 ++++++++++++++
 10 files changed, 77 insertions(+), 17 deletions(-)
 create mode 100644 include/scratchcpp-render/scratchcpp-render.h
 create mode 100644 src/global_functions.cpp
 create mode 100644 test/global_functions/CMakeLists.txt
 create mode 100644 test/global_functions/global_functions_test.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7830e5d..61f2475 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,6 +20,13 @@ target_compile_definitions(scratchcpp-render PRIVATE SCRATCHCPPRENDER_LIBRARY)
 
 linkQt(scratchcpp-render)
 
+target_sources(scratchcpp-render
+  PUBLIC
+    include/scratchcpp-render/scratchcpp-render.h
+)
+
+target_include_directories(scratchcpp-render PUBLIC include)
+
 include(build/SetUpLibscratchcpp.cmake)
 target_link_libraries(scratchcpp-render PRIVATE scratchcpp)
 
diff --git a/README.md b/README.md
index 36f35b9..17bc3ae 100644
--- a/README.md
+++ b/README.md
@@ -114,11 +114,15 @@ Button {
 }
 ```
 
-Please note that the ScratchCPP renderer only works with the basic scene graph render loop.
-Qt 6 uses the threaded render loop by default, so you'll have to disable it by calling this
-before constructing your application object:
+**The library must be initialized by the application.**
+**To initialize the library, call `scratchcpprender::init()` before constructing the Q(Gui)Application object.**
 ```cpp
-qputenv("QSG_RENDER_LOOP", "basic");
+#include <scratchcpp-render/scratchcpp-render.h>
+int main(int argc, char **argv) {
+    scratchcpprender::init();
+    QApplication a(argc, argv);
+    ...
+}
 ```
 
 <p align="right">(<a href="#readme-top">back to top</a>)</p>
diff --git a/include/scratchcpp-render/scratchcpp-render.h b/include/scratchcpp-render/scratchcpp-render.h
new file mode 100644
index 0000000..de80b15
--- /dev/null
+++ b/include/scratchcpp-render/scratchcpp-render.h
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#pragma once
+
+/*! \brief The main namespace of the library. */
+namespace scratchcpprender
+{
+
+/*! Initializes the library. Call this from main before constructing your Q(Gui)Application object. */
+void init();
+
+} // namespace scratchcpprender
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c133a57..48d5064 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -18,11 +18,12 @@ qt_add_qml_module(scratchcpp-render
 	shaders/sprite.frag
 	icons/enter.svg
     SOURCES
-        global.h
+    global.h
+    global_functions.cpp
 	projectloader.cpp
 	projectloader.h
-        projectscene.cpp
-        projectscene.h
+    projectscene.cpp
+    projectscene.h
 	stagemodel.cpp
 	stagemodel.h
 	spritemodel.cpp
@@ -44,8 +45,8 @@ qt_add_qml_module(scratchcpp-render
 	bitmapskin.h
 	svgskin.cpp
 	svgskin.h
-        renderedtarget.cpp
-        renderedtarget.h
+    renderedtarget.cpp
+    renderedtarget.h
 	targetpainter.cpp
 	targetpainter.h
 	scenemousearea.cpp
diff --git a/src/global_functions.cpp b/src/global_functions.cpp
new file mode 100644
index 0000000..38ff464
--- /dev/null
+++ b/src/global_functions.cpp
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+
+#include <QQuickWindow>
+#include <scratchcpp-render/scratchcpp-render.h>
+
+void scratchcpprender::init()
+{
+    qputenv("QSG_RENDER_LOOP", "basic");
+    QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
+}
diff --git a/src/penlayerpainter.cpp b/src/penlayerpainter.cpp
index 1ef6ea0..443b4ab 100644
--- a/src/penlayerpainter.cpp
+++ b/src/penlayerpainter.cpp
@@ -12,10 +12,8 @@ PenLayerPainter::PenLayerPainter(QOpenGLFramebufferObject *fbo)
 
 void PenLayerPainter::paint(QNanoPainter *painter)
 {
-    if (QThread::currentThread() != qApp->thread()) {
-        qFatal("Error: Rendering must happen in the GUI thread to work correctly. Please disable threaded render loop using qputenv(\"QSG_RENDER_LOOP\", \"basic\") before constructing your "
-               "application object.");
-    }
+    if (QThread::currentThread() != qApp->thread())
+        qFatal("Error: Rendering must happen in the GUI thread to work correctly. Did you initialize the library using scratchcpprender::init()?");
 
     QOpenGLContext *context = QOpenGLContext::currentContext();
     Q_ASSERT(context);
diff --git a/src/targetpainter.cpp b/src/targetpainter.cpp
index 5258b7f..d25b572 100644
--- a/src/targetpainter.cpp
+++ b/src/targetpainter.cpp
@@ -23,10 +23,8 @@ TargetPainter::~TargetPainter()
 
 void TargetPainter::paint(QNanoPainter *painter)
 {
-    if (QThread::currentThread() != qApp->thread()) {
-        qFatal("Error: Rendering must happen in the GUI thread to work correctly. Please disable threaded render loop using qputenv(\"QSG_RENDER_LOOP\", \"basic\") before constructing your "
-               "application object.");
-    }
+    if (QThread::currentThread() != qApp->thread())
+        qFatal("Error: Rendering must happen in the GUI thread to work correctly. Did you initialize the library using scratchcpprender::init()?");
 
     QOpenGLContext *context = QOpenGLContext::currentContext();
     Q_ASSERT(context);
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 463fd1f..dcd6bef 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -20,6 +20,7 @@ endfunction()
 
 add_subdirectory(mocks)
 
+add_subdirectory(global_functions)
 add_subdirectory(projectloader)
 add_subdirectory(renderedtarget)
 add_subdirectory(targetpainter)
diff --git a/test/global_functions/CMakeLists.txt b/test/global_functions/CMakeLists.txt
new file mode 100644
index 0000000..fb3359d
--- /dev/null
+++ b/test/global_functions/CMakeLists.txt
@@ -0,0 +1,15 @@
+add_executable(
+  global_functions_test
+  global_functions_test.cpp
+)
+
+target_link_libraries(
+  global_functions_test
+  GTest::gtest_main
+  GTest::gmock_main
+  scratchcpp-render
+  ${QT_LIBS}
+)
+
+add_test(global_functions_test)
+gtest_discover_tests(global_functions_test)
diff --git a/test/global_functions/global_functions_test.cpp b/test/global_functions/global_functions_test.cpp
new file mode 100644
index 0000000..273accd
--- /dev/null
+++ b/test/global_functions/global_functions_test.cpp
@@ -0,0 +1,14 @@
+#include <scratchcpp-render/scratchcpp-render.h>
+#include <QQuickWindow>
+
+#include "../common.h"
+
+TEST(GlobalFunctionsTest, Init)
+{
+    qputenv("QSG_RENDER_LOOP", "threaded");
+    QQuickWindow::setGraphicsApi(QSGRendererInterface::Software);
+
+    scratchcpprender::init();
+    ASSERT_EQ(qgetenv("QSG_RENDER_LOOP"), "basic");
+    ASSERT_EQ(QQuickWindow::graphicsApi(), QSGRendererInterface::OpenGL);
+}

From 8a5a00c4bc187624f7891e13b1e72ab3759934b3 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 25 Feb 2024 19:04:10 +0100
Subject: [PATCH 11/70] RenderedTarget: Fix incorrect assertions

---
 src/renderedtarget.cpp | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 067e420..438636f 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -379,7 +379,7 @@ QRectF RenderedTarget::getBoundsForBubble() const
     if (rect.height() > slice)
         rect.setBottom(rect.top() - slice);
 
-    Q_ASSERT(rect.height() <= 8);
+    Q_ASSERT(rect.height() <= 8 || std::abs(rect.bottom()) == std::numeric_limits<double>::infinity() || std::abs(rect.top()) == std::numeric_limits<double>::infinity());
     return QRectF(QPointF(rect.left(), rect.top()), QPointF(rect.right(), rect.bottom()));
 }
 
@@ -441,7 +441,6 @@ void RenderedTarget::mouseReleaseEvent(QMouseEvent *event)
 {
     m_clicked = false;
     Q_ASSERT(m_mouseArea);
-    Q_ASSERT(m_engine);
 
     // Stop dragging
     if (m_mouseArea->draggedSprite() == this)

From 5441775c71c03d6e14784fbc32252092f2671fe1 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 25 Feb 2024 19:06:04 +0100
Subject: [PATCH 12/70] SvgSkin: Do not create all textures when project loads

---
 src/svgskin.cpp | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/src/svgskin.cpp b/src/svgskin.cpp
index 7962ec8..1119bf1 100644
--- a/src/svgskin.cpp
+++ b/src/svgskin.cpp
@@ -28,11 +28,6 @@ SVGSkin::SVGSkin(libscratchcpp::Costume *costume, bool antialiasing) :
     const int i1 = std::log2(MAX_TEXTURE_DIMENSION / viewBox.width()) + INDEX_OFFSET;
     const int i2 = std::log2(MAX_TEXTURE_DIMENSION / viewBox.height()) + INDEX_OFFSET;
     m_maxIndex = std::min(i1, i2);
-
-    // Create all possible textures (the 1.0 scale is stored at INDEX_OFFSET)
-    // TODO: Is this necessary?
-    for (int i = 0; i <= m_maxIndex; i++)
-        createScaledTexture(i);
 }
 
 SVGSkin::~SVGSkin()

From 4b66e98f46846edd85f0787c1371c494bbb64a79 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 29 Feb 2024 21:14:05 +0100
Subject: [PATCH 13/70] Update libscratchcpp to latest master

---
 libscratchcpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libscratchcpp b/libscratchcpp
index 2e3d3df..08201fd 160000
--- a/libscratchcpp
+++ b/libscratchcpp
@@ -1 +1 @@
-Subproject commit 2e3d3df69476937958a83a01747c0c7c38a48f42
+Subproject commit 08201fdb18a73bfc19365836df50567f59eef8ee

From 417516e0057258400c23e09fc3b3930631e1cdd5 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 2 Mar 2024 14:04:09 +0100
Subject: [PATCH 14/70] fix #110: Do not use OpenGL functions directly

---
 src/penlayer.cpp                                 | 10 ++++++----
 src/penlayer.h                                   |  2 ++
 src/renderedtarget.cpp                           |  8 +++++++-
 src/renderedtarget.h                             |  1 +
 src/skin.cpp                                     |  7 +++++--
 src/texture.cpp                                  |  8 ++++++--
 test/targetpainter/targetpainter_test.cpp        |  7 +++++--
 .../textbubblepainter/textbubblepainter_test.cpp | 14 ++++++++++----
 test/texture/texture_test.cpp                    | 16 +++++++++++-----
 9 files changed, 53 insertions(+), 20 deletions(-)

diff --git a/src/penlayer.cpp b/src/penlayer.cpp
index 2809806..26f1918 100644
--- a/src/penlayer.cpp
+++ b/src/penlayer.cpp
@@ -13,6 +13,8 @@ PenLayer::PenLayer(QNanoQuickItem *parent) :
 {
     m_fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
     m_fboFormat.setSamples(m_antialiasingEnabled ? 4 : 0);
+
+    m_glF.initializeOpenGLFunctions();
 }
 
 PenLayer::~PenLayer()
@@ -65,10 +67,10 @@ void scratchcpprender::PenLayer::clear()
         return;
 
     m_fbo->bind();
-    glDisable(GL_SCISSOR_TEST);
-    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
-    glClear(GL_COLOR_BUFFER_BIT);
-    glEnable(GL_SCISSOR_TEST);
+    m_glF.glDisable(GL_SCISSOR_TEST);
+    m_glF.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+    m_glF.glClear(GL_COLOR_BUFFER_BIT);
+    m_glF.glEnable(GL_SCISSOR_TEST);
     m_fbo->release();
 
     update();
diff --git a/src/penlayer.h b/src/penlayer.h
index 7a98a0b..b8b5987 100644
--- a/src/penlayer.h
+++ b/src/penlayer.h
@@ -5,6 +5,7 @@
 #include <ipenlayer.h>
 #include <QOpenGLFramebufferObject>
 #include <QOpenGLPaintDevice>
+#include <QOpenGLFunctions>
 #include <QPainter>
 #include <scratchcpp/iengine.h>
 
@@ -49,6 +50,7 @@ class PenLayer : public IPenLayer
         std::unique_ptr<QOpenGLFramebufferObject> m_fbo;
         std::unique_ptr<QOpenGLPaintDevice> m_paintDevice;
         QOpenGLFramebufferObjectFormat m_fboFormat;
+        QOpenGLFunctions m_glF;
 };
 
 } // namespace scratchcpprender
diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 438636f..bf2b5b0 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -513,6 +513,12 @@ void RenderedTarget::clearGraphicEffects()
 void RenderedTarget::updateHullPoints(QOpenGLFramebufferObject *fbo)
 {
     Q_ASSERT(fbo);
+
+    if (!m_glF) {
+        m_glF = std::make_unique<QOpenGLFunctions>();
+        m_glF->initializeOpenGLFunctions();
+    }
+
     int width = fbo->width();
     int height = fbo->height();
     m_hullPoints.clear();
@@ -521,7 +527,7 @@ void RenderedTarget::updateHullPoints(QOpenGLFramebufferObject *fbo)
     // Read pixels from framebuffer
     size_t size = width * height * 4;
     GLubyte *pixelData = new GLubyte[size];
-    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixelData);
+    m_glF->glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixelData);
 
     // Flip vertically
     int rowSize = width * 4;
diff --git a/src/renderedtarget.h b/src/renderedtarget.h
index 75ed7af..d11f4d2 100644
--- a/src/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -125,6 +125,7 @@ class RenderedTarget : public IRenderedTarget
         Skin *m_skin = nullptr;
         Texture m_texture;
         Texture m_oldTexture;
+        std::unique_ptr<QOpenGLFunctions> m_glF;
         std::unordered_map<ShaderManager::Effect, double> m_graphicEffects;
         double m_size = 1;
         double m_x = 0;
diff --git a/src/skin.cpp b/src/skin.cpp
index f28fd30..dd6c150 100644
--- a/src/skin.cpp
+++ b/src/skin.cpp
@@ -18,6 +18,9 @@ Texture Skin::createAndPaintTexture(int width, int height, bool multisampled)
     if (!context || !context->isValid() || (width <= 0 || height <= 0))
         return Texture();
 
+    QOpenGLFunctions glF(context);
+    glF.initializeOpenGLFunctions();
+
     // Create offscreen surface
     QOffscreenSurface surface;
     surface.setFormat(context->format());
@@ -48,8 +51,8 @@ Texture Skin::createAndPaintTexture(int width, int height, bool multisampled)
     QPainter painter(&device);
     painter.beginNativePainting();
     painter.setRenderHint(QPainter::Antialiasing, false);
-    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
-    glClear(GL_COLOR_BUFFER_BIT);
+    glF.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+    glF.glClear(GL_COLOR_BUFFER_BIT);
 
     // Call the skin-specific paint method
     paint(&painter);
diff --git a/src/texture.cpp b/src/texture.cpp
index a0bf2da..3bca9c2 100644
--- a/src/texture.cpp
+++ b/src/texture.cpp
@@ -114,8 +114,12 @@ QImage Texture::toImage() const
 
 void Texture::release()
 {
-    if (m_isValid) {
-        glDeleteTextures(1, &m_handle);
+    QOpenGLContext *context = QOpenGLContext::currentContext();
+
+    if (m_isValid && context) {
+        QOpenGLExtraFunctions glF(context);
+        glF.initializeOpenGLFunctions();
+        glF.glDeleteTextures(1, &m_handle);
         m_isValid = false;
     }
 }
diff --git a/test/targetpainter/targetpainter_test.cpp b/test/targetpainter/targetpainter_test.cpp
index 2d7d1df..1d4c7b3 100644
--- a/test/targetpainter/targetpainter_test.cpp
+++ b/test/targetpainter/targetpainter_test.cpp
@@ -37,6 +37,9 @@ TEST_F(TargetPainterTest, Paint)
     QOpenGLFramebufferObjectFormat format;
     format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
 
+    QOpenGLExtraFunctions glF(&m_context);
+    glF.initializeOpenGLFunctions();
+
     // Begin painting reference
     QNanoPainter refPainter;
     QOpenGLFramebufferObject refFbo(40, 60, format);
@@ -87,8 +90,8 @@ TEST_F(TargetPainterTest, Paint)
     ASSERT_EQ(fbo.toImage(), refFbo.toImage());
 
     // Paint with color effects
-    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
-    glClear(GL_COLOR_BUFFER_BIT);
+    glF.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+    glF.glClear(GL_COLOR_BUFFER_BIT);
     effects[ShaderManager::Effect::Color] = 46;
     effects[ShaderManager::Effect::Brightness] = 20;
     effects[ShaderManager::Effect::Ghost] = 84;
diff --git a/test/textbubblepainter/textbubblepainter_test.cpp b/test/textbubblepainter/textbubblepainter_test.cpp
index 739095b..0e38e1b 100644
--- a/test/textbubblepainter/textbubblepainter_test.cpp
+++ b/test/textbubblepainter/textbubblepainter_test.cpp
@@ -33,6 +33,9 @@ TEST_F(TextBubblePainterTest, PaintSayBubble)
     QOffscreenSurface surface;
     createContextAndSurface(&context, &surface);
 
+    QOpenGLExtraFunctions glF(&context);
+    glF.initializeOpenGLFunctions();
+
     QOpenGLFramebufferObjectFormat format;
     format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
 
@@ -40,8 +43,8 @@ TEST_F(TextBubblePainterTest, PaintSayBubble)
     QNanoPainter painter;
     QOpenGLFramebufferObject fbo(425, 250, format);
     fbo.bind();
-    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
-    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+    glF.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+    glF.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
     painter.beginFrame(fbo.width(), fbo.height());
     painter.setAntialias(0);
 
@@ -77,6 +80,9 @@ TEST_F(TextBubblePainterTest, PaintThinkBubble)
     QOffscreenSurface surface;
     createContextAndSurface(&context, &surface);
 
+    QOpenGLExtraFunctions glF(&context);
+    glF.initializeOpenGLFunctions();
+
     QOpenGLFramebufferObjectFormat format;
     format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
 
@@ -84,8 +90,8 @@ TEST_F(TextBubblePainterTest, PaintThinkBubble)
     QNanoPainter painter;
     QOpenGLFramebufferObject fbo(156, 117, format);
     fbo.bind();
-    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
-    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+    glF.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+    glF.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
     painter.beginFrame(fbo.width(), fbo.height());
     painter.setAntialias(0);
 
diff --git a/test/texture/texture_test.cpp b/test/texture/texture_test.cpp
index ffdff19..4f639ec 100644
--- a/test/texture/texture_test.cpp
+++ b/test/texture/texture_test.cpp
@@ -49,6 +49,9 @@ TEST(TextureTest, ToImage)
     Q_ASSERT(surface.isValid());
     context.makeCurrent(&surface);
 
+    QOpenGLExtraFunctions glF(&context);
+    glF.initializeOpenGLFunctions();
+
     QOpenGLFramebufferObjectFormat format;
     format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
 
@@ -59,8 +62,8 @@ TEST(TextureTest, ToImage)
     QPainter painter(&device);
     painter.beginNativePainting();
     painter.setRenderHint(QPainter::Antialiasing, false);
-    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
-    glClear(GL_COLOR_BUFFER_BIT);
+    glF.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+    glF.glClear(GL_COLOR_BUFFER_BIT);
     painter.drawEllipse(0, 0, fbo.width(), fbo.height());
     painter.endNativePainting();
     painter.end();
@@ -86,15 +89,18 @@ TEST(TextureTest, Release)
     Q_ASSERT(surface.isValid());
     context.makeCurrent(&surface);
 
+    QOpenGLExtraFunctions glF(&context);
+    glF.initializeOpenGLFunctions();
+
     QOpenGLFramebufferObject fbo(1, 1);
     GLuint handle = fbo.takeTexture();
-    ASSERT_TRUE(glIsTexture(handle));
+    ASSERT_TRUE(glF.glIsTexture(handle));
 
     Texture tex(handle, fbo.width(), fbo.height());
-    ASSERT_TRUE(glIsTexture(handle));
+    ASSERT_TRUE(glF.glIsTexture(handle));
 
     tex.release();
-    ASSERT_FALSE(glIsTexture(handle));
+    ASSERT_FALSE(glF.glIsTexture(handle));
     ASSERT_FALSE(tex.isValid());
 
     context.doneCurrent();

From 0a77257f265019ebc9a0e31900b61813c49601e8 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 2 Mar 2024 21:32:42 +0100
Subject: [PATCH 15/70] Fix crash during OpenGL initialization in PenLayer

---
 src/penlayer.cpp | 15 +++++++++------
 src/penlayer.h   |  2 +-
 2 files changed, 10 insertions(+), 7 deletions(-)

diff --git a/src/penlayer.cpp b/src/penlayer.cpp
index 26f1918..66a0c2e 100644
--- a/src/penlayer.cpp
+++ b/src/penlayer.cpp
@@ -13,8 +13,6 @@ PenLayer::PenLayer(QNanoQuickItem *parent) :
 {
     m_fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
     m_fboFormat.setSamples(m_antialiasingEnabled ? 4 : 0);
-
-    m_glF.initializeOpenGLFunctions();
 }
 
 PenLayer::~PenLayer()
@@ -66,11 +64,16 @@ void scratchcpprender::PenLayer::clear()
     if (!m_fbo)
         return;
 
+    if (!m_glF) {
+        m_glF = std::make_unique<QOpenGLFunctions>();
+        m_glF->initializeOpenGLFunctions();
+    }
+
     m_fbo->bind();
-    m_glF.glDisable(GL_SCISSOR_TEST);
-    m_glF.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
-    m_glF.glClear(GL_COLOR_BUFFER_BIT);
-    m_glF.glEnable(GL_SCISSOR_TEST);
+    m_glF->glDisable(GL_SCISSOR_TEST);
+    m_glF->glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+    m_glF->glClear(GL_COLOR_BUFFER_BIT);
+    m_glF->glEnable(GL_SCISSOR_TEST);
     m_fbo->release();
 
     update();
diff --git a/src/penlayer.h b/src/penlayer.h
index b8b5987..f1243d9 100644
--- a/src/penlayer.h
+++ b/src/penlayer.h
@@ -50,7 +50,7 @@ class PenLayer : public IPenLayer
         std::unique_ptr<QOpenGLFramebufferObject> m_fbo;
         std::unique_ptr<QOpenGLPaintDevice> m_paintDevice;
         QOpenGLFramebufferObjectFormat m_fboFormat;
-        QOpenGLFunctions m_glF;
+        std::unique_ptr<QOpenGLFunctions> m_glF;
 };
 
 } // namespace scratchcpprender

From 91de47f293e9eb315e9701d1f7a4443b2790f6fd Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 2 Mar 2024 21:51:44 +0100
Subject: [PATCH 16/70] fix #104: Properly clean up before loading another
 project

---
 src/projectloader.cpp                     | 49 +++++++++++++++++++----
 src/projectloader.h                       |  1 +
 test/projectloader/projectloader_test.cpp | 21 +++++-----
 3 files changed, 54 insertions(+), 17 deletions(-)

diff --git a/src/projectloader.cpp b/src/projectloader.cpp
index 9def881..356f348 100644
--- a/src/projectloader.cpp
+++ b/src/projectloader.cpp
@@ -64,6 +64,40 @@ void ProjectLoader::setFileName(const QString &newFileName)
 
     m_fileName = newFileName;
 
+    // Stop the project
+    if (m_engine)
+        m_engine->stop();
+
+    // Delete old sprites
+    for (SpriteModel *sprite : m_sprites)
+        sprite->deleteLater();
+
+    m_sprites.clear();
+    emit spritesChanged();
+
+    // Delete old clones
+    for (SpriteModel *clone : m_clones)
+        deleteCloneObject(clone);
+
+    m_clones.clear();
+    emit clonesChanged();
+
+    // Delete old monitors
+    for (MonitorModel *monitor : m_monitors) {
+        emit monitorRemoved(monitor);
+        monitor->deleteLater();
+    }
+
+    m_monitors.clear();
+    emit monitorsChanged();
+
+    // Clear the engine
+    if (m_engine)
+        m_engine->clear();
+
+    m_engine = nullptr;
+    emit engineChanged();
+
     m_project.setScratchVersion(ScratchVersion::Scratch3);
     m_project.setFileName(m_fileName.toStdString());
     m_loadStatus = false;
@@ -198,12 +232,6 @@ void ProjectLoader::load()
     m_engineMutex.lock();
     m_engine = m_project.engine().get();
 
-    // Delete old sprites
-    for (SpriteModel *sprite : m_sprites)
-        sprite->deleteLater();
-
-    m_sprites.clear();
-
     if (!m_engine || m_stopLoading) {
         m_engineMutex.unlock();
         emit fileNameChanged();
@@ -313,13 +341,18 @@ void ProjectLoader::addClone(SpriteModel *model)
     emit clonesChanged();
 }
 
-void ProjectLoader::deleteClone(SpriteModel *model)
+void ProjectLoader::deleteCloneObject(SpriteModel *model)
 {
-    m_clones.removeAll(model);
     emit cloneDeleted(model);
     Q_ASSERT(model->renderedTarget());
     model->renderedTarget()->deinitClone();
     model->deleteLater();
+}
+
+void ProjectLoader::deleteClone(SpriteModel *model)
+{
+    m_clones.removeAll(model);
+    deleteCloneObject(model);
     emit clonesChanged();
 }
 
diff --git a/src/projectloader.h b/src/projectloader.h
index 06731ae..198497c 100644
--- a/src/projectloader.h
+++ b/src/projectloader.h
@@ -120,6 +120,7 @@ class ProjectLoader : public QObject
         void initTimer();
         void redraw();
         void addClone(SpriteModel *model);
+        void deleteCloneObject(SpriteModel *model);
         void deleteClone(SpriteModel *model);
         void addMonitor(libscratchcpp::Monitor *monitor);
         void removeMonitor(libscratchcpp::Monitor *monitor, libscratchcpp::IMonitorHandler *iface);
diff --git a/test/projectloader/projectloader_test.cpp b/test/projectloader/projectloader_test.cpp
index 23ae4a0..64e83da 100644
--- a/test/projectloader/projectloader_test.cpp
+++ b/test/projectloader/projectloader_test.cpp
@@ -29,6 +29,7 @@ class ProjectLoaderTest : public testing::Test
             QSignalSpy engineSpy(loader, &ProjectLoader::engineChanged);
             QSignalSpy stageSpy(loader, &ProjectLoader::stageChanged);
             QSignalSpy spritesSpy(loader, &ProjectLoader::spritesChanged);
+            QSignalSpy clonesSpy(loader, &ProjectLoader::clonesChanged);
             QSignalSpy monitorsSpy(loader, &ProjectLoader::monitorsChanged);
             QSignalSpy monitorAddedSpy(loader, &ProjectLoader::monitorAdded);
 
@@ -37,10 +38,11 @@ class ProjectLoaderTest : public testing::Test
             ASSERT_EQ(fileNameSpy.count(), 1);
             ASSERT_EQ(loadStatusSpy.count(), 1);
             ASSERT_TRUE(loadingFinishedSpy.empty());
-            ASSERT_TRUE(engineSpy.empty());
+            ASSERT_EQ(engineSpy.count(), 1);
             ASSERT_TRUE(stageSpy.empty());
-            ASSERT_TRUE(spritesSpy.empty());
-            ASSERT_TRUE(monitorsSpy.empty());
+            ASSERT_EQ(spritesSpy.count(), 1);
+            ASSERT_EQ(clonesSpy.count(), 1);
+            ASSERT_EQ(monitorsSpy.count(), 1);
             ASSERT_TRUE(monitorAddedSpy.empty());
             ASSERT_EQ(loader->fileName(), fileName);
             ASSERT_FALSE(loader->loadStatus());
@@ -52,10 +54,11 @@ class ProjectLoaderTest : public testing::Test
             ASSERT_EQ(fileNameSpy.count(), 1);
             ASSERT_EQ(loadStatusSpy.count(), 2);
             ASSERT_EQ(loadingFinishedSpy.count(), 1);
-            ASSERT_EQ(engineSpy.count(), 1);
+            ASSERT_EQ(engineSpy.count(), 2);
             ASSERT_EQ(stageSpy.count(), 1);
-            ASSERT_EQ(spritesSpy.count(), 1);
-            ASSERT_EQ(monitorsSpy.count(), loader->monitorList().size());
+            ASSERT_EQ(spritesSpy.count(), 2);
+            ASSERT_EQ(clonesSpy.count(), 1);
+            ASSERT_EQ(monitorsSpy.count(), loader->monitorList().size() + 1);
             ASSERT_EQ(monitorAddedSpy.count(), loader->monitorList().size());
         }
 };
@@ -113,13 +116,13 @@ TEST_F(ProjectLoaderTest, Clones)
     load(&loader, "clones.sb3");
     ASSERT_TRUE(cloneCreatedSpy.empty());
     ASSERT_TRUE(cloneDeletedSpy.empty());
-    ASSERT_TRUE(clonesChangedSpy.empty());
+    ASSERT_EQ(clonesChangedSpy.count(), 1);
 
     auto engine = loader.engine();
     engine->run();
     ASSERT_EQ(cloneCreatedSpy.count(), 3);
     ASSERT_EQ(cloneDeletedSpy.count(), 0);
-    ASSERT_EQ(clonesChangedSpy.count(), 3);
+    ASSERT_EQ(clonesChangedSpy.count(), 4);
 
     const auto &sprites = loader.spriteList();
     const auto &clones = loader.cloneList();
@@ -138,7 +141,7 @@ TEST_F(ProjectLoaderTest, Clones)
     clones[1]->sprite()->deleteClone();
     ASSERT_EQ(cloneCreatedSpy.count(), 3);
     ASSERT_EQ(cloneDeletedSpy.count(), 1);
-    ASSERT_EQ(clonesChangedSpy.count(), 4);
+    ASSERT_EQ(clonesChangedSpy.count(), 5);
     ASSERT_EQ(clones.size(), 2);
 }
 

From 9b219150cbe353ff33ffaab5942b752f853b9c42 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 2 Mar 2024 21:53:57 +0100
Subject: [PATCH 17/70] fix #105: Ignore removed sprites in MouseEventHandler

---
 src/mouseeventhandler.cpp | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/mouseeventhandler.cpp b/src/mouseeventhandler.cpp
index a8db22e..18876ea 100644
--- a/src/mouseeventhandler.cpp
+++ b/src/mouseeventhandler.cpp
@@ -113,6 +113,14 @@ void scratchcpprender::MouseEventHandler::getSprites()
         Q_ASSERT(sprite->scratchTarget());
         m_sprites.push_back(sprite);
     }
+
+    // Make sure the clicked and hovered item pointers are in the list
+    // If not, make them nullptr
+    if (std::find(m_sprites.begin(), m_sprites.end(), m_clickedItem) == m_sprites.end())
+        m_clickedItem = nullptr;
+
+    if (std::find(m_sprites.begin(), m_sprites.end(), m_hoveredItem) == m_sprites.end())
+        m_hoveredItem = nullptr;
 }
 
 void scratchcpprender::MouseEventHandler::addClone(SpriteModel *model)

From 5b0054e576c43122edad3333bac63588f5d95895 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 2 Mar 2024 22:17:49 +0100
Subject: [PATCH 18/70] fix #109: Reset stage model before loading another
 project

---
 src/projectloader.cpp | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/projectloader.cpp b/src/projectloader.cpp
index 356f348..7171d59 100644
--- a/src/projectloader.cpp
+++ b/src/projectloader.cpp
@@ -68,6 +68,9 @@ void ProjectLoader::setFileName(const QString &newFileName)
     if (m_engine)
         m_engine->stop();
 
+    // Reset stage model
+    m_stage.init(nullptr);
+
     // Delete old sprites
     for (SpriteModel *sprite : m_sprites)
         sprite->deleteLater();

From 026716c973c1d2613f17960243ea63fcd869bd72 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 2 Mar 2024 22:22:00 +0100
Subject: [PATCH 19/70] fix #106: Fix backdrop loading when loading another
 project

---
 src/renderedtarget.cpp                      | 16 ++++++++++++++++
 src/stagemodel.cpp                          |  7 ++-----
 test/renderedtarget/renderedtarget_test.cpp |  4 ++--
 test/target_models/stagemodel_test.cpp      |  1 -
 4 files changed, 20 insertions(+), 8 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index bf2b5b0..20262ce 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -199,6 +199,22 @@ void RenderedTarget::setEngine(IEngine *newEngine)
         return;
 
     m_engine = newEngine;
+    m_costume = nullptr;
+    m_costumesLoaded = false;
+
+    if (!m_skinsInherited) {
+        for (const auto &[costume, skin] : m_skins)
+            delete skin;
+
+        m_skins.clear();
+    }
+
+    m_skin = nullptr;
+    m_texture = Texture();
+    m_oldTexture = Texture();
+    clearGraphicEffects();
+    m_hullPoints.clear();
+
     emit engineChanged();
 }
 
diff --git a/src/stagemodel.cpp b/src/stagemodel.cpp
index 5c600ec..1379c80 100644
--- a/src/stagemodel.cpp
+++ b/src/stagemodel.cpp
@@ -79,10 +79,8 @@ void StageModel::onBubbleTextChanged(const std::string &text)
 
 void StageModel::loadCostume()
 {
-    if (m_renderedTarget && m_stage) {
-        if (m_stage)
-            m_renderedTarget->updateCostume(m_stage->currentCostume().get());
-    }
+    if (m_renderedTarget && m_stage)
+        m_renderedTarget->updateCostume(m_stage->currentCostume().get());
 }
 
 libscratchcpp::Stage *StageModel::stage() const
@@ -101,7 +99,6 @@ void StageModel::setRenderedTarget(IRenderedTarget *newRenderedTarget)
         return;
 
     m_renderedTarget = newRenderedTarget;
-    loadCostume();
 
     emit renderedTargetChanged();
 }
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index b595ba8..fb02831 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -56,6 +56,8 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     createContextAndSurface(&context, &surface);
     RenderedTarget parent; // a parent item is needed for setVisible() to work
     RenderedTarget target(&parent);
+    EngineMock engine;
+    target.setEngine(&engine);
     QSignalSpy mirrorHorizontallySpy(&target, &RenderedTarget::mirrorHorizontallyChanged);
     ASSERT_FALSE(target.costumesLoaded());
 
@@ -73,8 +75,6 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     stage.addCostume(costume);
     target.loadCostumes();
     ASSERT_TRUE(target.costumesLoaded());
-    EngineMock engine;
-    target.setEngine(&engine);
 
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
diff --git a/test/target_models/stagemodel_test.cpp b/test/target_models/stagemodel_test.cpp
index 8d73cda..66a50cc 100644
--- a/test/target_models/stagemodel_test.cpp
+++ b/test/target_models/stagemodel_test.cpp
@@ -123,7 +123,6 @@ TEST(StageModelTest, RenderedTarget)
 
     RenderedTargetMock renderedTarget;
     QSignalSpy spy(&model, &StageModel::renderedTargetChanged);
-    EXPECT_CALL(renderedTarget, updateCostume(c2.get()));
     model.setRenderedTarget(&renderedTarget);
     ASSERT_EQ(spy.count(), 1);
     ASSERT_EQ(model.renderedTarget(), &renderedTarget);

From e18b4bc8811dc024f9f3cc5651e2e945e513653f Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 2 Mar 2024 22:30:24 +0100
Subject: [PATCH 20/70] ProjectLoader: Repaint stage before loading another
 project

---
 src/projectloader.cpp | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/projectloader.cpp b/src/projectloader.cpp
index 7171d59..84a7544 100644
--- a/src/projectloader.cpp
+++ b/src/projectloader.cpp
@@ -71,6 +71,9 @@ void ProjectLoader::setFileName(const QString &newFileName)
     // Reset stage model
     m_stage.init(nullptr);
 
+    if (m_stage.renderedTarget())
+        m_stage.renderedTarget()->update();
+
     // Delete old sprites
     for (SpriteModel *sprite : m_sprites)
         sprite->deleteLater();

From a586391572989627e73e4f128913e50050c6bf9b Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 2 Mar 2024 22:33:36 +0100
Subject: [PATCH 21/70] ProjectPlayer: Hide pen layer while loading

---
 src/ProjectPlayer.qml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml
index 4765be9..4e168d9 100644
--- a/src/ProjectPlayer.qml
+++ b/src/ProjectPlayer.qml
@@ -133,6 +133,7 @@ ProjectScene {
             id: projectPenLayer
             engine: loader.engine
             anchors.fill: parent
+            visible: !priv.loading
         }
 
         Component {

From aebe016400665ce764bf1de087339da21813d3a5 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 5 Mar 2024 18:46:47 +0100
Subject: [PATCH 22/70] Add definitions for project version

---
 CMakeLists.txt | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 61f2475..f61624d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,6 @@
 cmake_minimum_required(VERSION 3.14)
 
-project(scratchcpp-render VERSION 1.0.0 LANGUAGES CXX)
+project(scratchcpp-render VERSION 0.5.0 LANGUAGES CXX)
 
 set(CMAKE_INCLUDE_CURRENT_DIR ON)
 set(CMAKE_AUTOUIC ON)
@@ -17,6 +17,10 @@ qt_standard_project_setup(REQUIRES 6.6)
 add_subdirectory(src)
 
 target_compile_definitions(scratchcpp-render PRIVATE SCRATCHCPPRENDER_LIBRARY)
+target_compile_definitions(scratchcpp-render PRIVATE SCRATCHCPPRENDER_VERSION="${PROJECT_VERSION}")
+target_compile_definitions(scratchcpp-render PRIVATE SCRATCHCPPRENDER_VERSION_MAJOR=${PROJECT_VERSION_MAJOR})
+target_compile_definitions(scratchcpp-render PRIVATE SCRATCHCPPRENDER_VERSION_MINOR=${PROJECT_VERSION_MINOR})
+target_compile_definitions(scratchcpp-render PRIVATE SCRATCHCPPRENDER_VERSION_PATCH=${PROJECT_VERSION_PATCH})
 
 linkQt(scratchcpp-render)
 

From 6c8855b3a196ab16cee26ea8fbd522579d8ddcc1 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 5 Mar 2024 18:47:11 +0100
Subject: [PATCH 23/70] Add version API

---
 include/scratchcpp-render/scratchcpp-render.h | 14 +++++++++++++
 src/global_functions.cpp                      | 21 +++++++++++++++++++
 2 files changed, 35 insertions(+)

diff --git a/include/scratchcpp-render/scratchcpp-render.h b/include/scratchcpp-render/scratchcpp-render.h
index de80b15..a2bfad1 100644
--- a/include/scratchcpp-render/scratchcpp-render.h
+++ b/include/scratchcpp-render/scratchcpp-render.h
@@ -2,6 +2,8 @@
 
 #pragma once
 
+#include <string>
+
 /*! \brief The main namespace of the library. */
 namespace scratchcpprender
 {
@@ -9,4 +11,16 @@ namespace scratchcpprender
 /*! Initializes the library. Call this from main before constructing your Q(Gui)Application object. */
 void init();
 
+/*! Returns the version string of the library. */
+const std::string &version();
+
+/*! Returns the major version of the library. */
+int majorVersion();
+
+/*! Returns the minor version of the library. */
+int minorVersion();
+
+/*! Returns the patch version of the library. */
+int patchVersion();
+
 } // namespace scratchcpprender
diff --git a/src/global_functions.cpp b/src/global_functions.cpp
index 38ff464..3dc93a1 100644
--- a/src/global_functions.cpp
+++ b/src/global_functions.cpp
@@ -8,3 +8,24 @@ void scratchcpprender::init()
     qputenv("QSG_RENDER_LOOP", "basic");
     QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
 }
+
+const std::string &scratchcpprender::version()
+{
+    static const std::string ret = SCRATCHCPPRENDER_VERSION;
+    return ret;
+}
+
+int scratchcpprender::majorVersion()
+{
+    return SCRATCHCPPRENDER_VERSION_MAJOR;
+}
+
+int scratchcpprender::minorVersion()
+{
+    return SCRATCHCPPRENDER_VERSION_MINOR;
+}
+
+int scratchcpprender::patchVersion()
+{
+    return SCRATCHCPPRENDER_VERSION_PATCH;
+}

From 67502723fd0f0fce16df17210dcde5b3db52604b Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 7 Mar 2024 15:46:11 +0100
Subject: [PATCH 24/70] Update libscratchcpp to latest master

---
 libscratchcpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libscratchcpp b/libscratchcpp
index 08201fd..9fbd24f 160000
--- a/libscratchcpp
+++ b/libscratchcpp
@@ -1 +1 @@
-Subproject commit 08201fdb18a73bfc19365836df50567f59eef8ee
+Subproject commit 9fbd24f1abc9a225db8e73088a85739fe8a8dfd0

From 0461bba23df81714bd08a500f74a58704ab0572d Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 17 Mar 2024 21:30:06 +0100
Subject: [PATCH 25/70] BitmapSkin: Fix format string conversion

---
 src/bitmapskin.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/bitmapskin.cpp b/src/bitmapskin.cpp
index 89b92a3..d8b1c98 100644
--- a/src/bitmapskin.cpp
+++ b/src/bitmapskin.cpp
@@ -21,7 +21,7 @@ BitmapSkin::BitmapSkin(libscratchcpp::Costume *costume) :
 
     {
         QImageReader reader(&buffer);
-        format = reader.format();
+        format = reader.format().toStdString().c_str();
     }
 
     buffer.close();

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 26/70] 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<void()>(redrawHandler));
-
-    auto addMonitorHandler = std::bind(&ProjectLoader::addMonitor, this, std::placeholders::_1);
-    m_engine->setAddMonitorHandler(std::function<void(Monitor *)>(addMonitorHandler));
-
-    auto removeMonitorHandler = std::bind(&ProjectLoader::removeMonitor, this, std::placeholders::_1, std::placeholders::_2);
-    m_engine->setRemoveMonitorHandler(std::function<void(Monitor *, IMonitorHandler *)>(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<libscratchcpp::Sprite *> &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<libscratchcpp::Sprite *> &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<libscratchcpp::Sprite *> &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<libscratchcpp::Sprite *> &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<void()> &), (override));
+        MOCK_METHOD(sigslot::signal<> &, aboutToRender, (), (override));
+        MOCK_METHOD(sigslot::signal<VirtualMachine *> &, threadAboutToStop, (), (override));
 
         MOCK_METHOD(bool, isRunning, (), (const, override));
 
@@ -86,8 +87,10 @@ class EngineMock : public IEngine
 
         MOCK_METHOD(void, registerSection, (std::shared_ptr<IBlockSection>), (override));
         MOCK_METHOD(unsigned int, functionIndex, (BlockFunc), (override));
+        MOCK_METHOD(const std::vector<BlockFunc> &, 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<Block>), (override));
         MOCK_METHOD(void, addKeyPressScript, (std::shared_ptr<Block>, int), (override));
         MOCK_METHOD(void, addTargetClickScript, (std::shared_ptr<Block>), (override));
+        MOCK_METHOD(void, addWhenGreaterThanScript, (std::shared_ptr<Block>), (override));
 
         MOCK_METHOD(const std::vector<std::shared_ptr<Target>> &, targets, (), (const, override));
         MOCK_METHOD(void, setTargets, (const std::vector<std::shared_ptr<Target>> &), (override));
@@ -123,14 +127,11 @@ class EngineMock : public IEngine
 
         MOCK_METHOD(const std::vector<std::shared_ptr<Monitor>> &, monitors, (), (const, override));
         MOCK_METHOD(void, setMonitors, (const std::vector<std::shared_ptr<Monitor>> &), (override));
-        MOCK_METHOD(void, setAddMonitorHandler, (const std::function<void(Monitor *)> &), (override));
-        MOCK_METHOD(void, setRemoveMonitorHandler, (const std::function<void(Monitor *, IMonitorHandler *)> &), (override));
+        MOCK_METHOD(sigslot::signal<Monitor *> &, monitorAdded, (), (override));
+        MOCK_METHOD((sigslot::signal<Monitor *, IMonitorHandler *> &), monitorRemoved, (), (override));
 
-        MOCK_METHOD(const std::function<void(const std::string &)> &, questionAsked, (), (const, override));
-        MOCK_METHOD(void, setQuestionAsked, (const std::function<void(const std::string &)> &), (override));
-
-        MOCK_METHOD(const std::function<void(const std::string &)> &, questionAnswered, (), (const, override));
-        MOCK_METHOD(void, setQuestionAnswered, (const std::function<void(const std::string &)> &), (override));
+        MOCK_METHOD(sigslot::signal<const std::string &> &, questionAsked, (), (override));
+        MOCK_METHOD(sigslot::signal<const std::string &> &, questionAnswered, (), (override));
 
         MOCK_METHOD(std::vector<std::string> &, extensions, (), (const, override));
         MOCK_METHOD(void, setExtensions, (const std::vector<std::string> &), (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<ListMonitorModel *>(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<void(const std::string &)> f = std::bind(&AnswerQuestionMock::answer, &mock, std::placeholders::_1);
-    EXPECT_CALL(engine, questionAnswered()).WillOnce(ReturnRef(f));
+    sigslot::signal<const std::string &> 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 27/70] 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<QPoint> &CpuTextureManager::getTextureConvexHullPoints(const Texture &texture)
+{
+    static const std::vector<QPoint> 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<QPoint> &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 <QPoint>
+#include <QtOpenGL>
+#include <unordered_map>
+
+namespace scratchcpprender
+{
+
+class Texture;
+
+class CpuTextureManager
+{
+    public:
+        CpuTextureManager();
+        ~CpuTextureManager();
+
+        GLubyte *getTextureData(const Texture &texture);
+        const std::vector<QPoint> &getTextureConvexHullPoints(const Texture &texture);
+
+    private:
+        bool addTexture(const Texture &texture);
+
+        std::unordered_map<GLuint, GLubyte *> m_textureData;
+        std::unordered_map<GLuint, std::vector<QPoint>> 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 <cputexturemanager.h>
+#include <texture.h>
+#include <qnanopainter.h>
+
+#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<QOpenGLFramebufferObject>(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<QOpenGLFramebufferObject> 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<QPoint> refHullPoints1 = { { 1, 1 }, { 2, 1 }, { 3, 1 }, { 1, 2 }, { 3, 2 }, { 1, 3 }, { 2, 3 }, { 3, 3 } };
+
+    static const std::vector<QPoint> 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 28/70] 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<QPointF> &hullPoints() const = 0;
+        virtual const std::vector<QPoint> &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<RenderedTarget *>(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<double>::infinity();
     double bottom = std::numeric_limits<double>::infinity();
 
-    for (const QPointF &point : m_hullPoints) {
+    const std::vector<QPoint> &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<QPoint> &RenderedTarget::hullPoints() const
 {
-    Q_ASSERT(fbo);
-
-    if (!m_glF) {
-        m_glF = std::make_unique<QOpenGLFunctions>();
-        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<RenderedTarget *>(this)->updateHullPoints();
 
-const std::vector<QPointF> &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<QPoint> &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<CpuTextureManager>();
+
+    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<QPointF> &hullPoints() const override;
+        const std::vector<QPoint> &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<CpuTextureManager> m_textureManager; // NOTE: Use textureManager()!
         std::unique_ptr<QOpenGLFunctions> m_glF;
         std::unordered_map<ShaderManager::Effect, double> m_graphicEffects;
         double m_size = 1;
@@ -138,7 +142,8 @@ class RenderedTarget : public IRenderedTarget
         double m_stageScale = 1;
         qreal m_maximumWidth = std::numeric_limits<double>::infinity();
         qreal m_maximumHeight = std::numeric_limits<double>::infinity();
-        std::vector<QPointF> m_hullPoints;
+        bool m_convexHullDirty = true;
+        std::vector<QPoint> 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<QPointF> &, hullPoints, (), (const, override));
+        MOCK_METHOD(const std::vector<QPoint> &, 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<Costume>("", "", "png");
+    std::string costumeData = readFileStr("image.png");
+    costume->setData(costumeData.size(), static_cast<void *>(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<QPointF>({ { 1, 1 }, { 2, 1 }, { 3, 1 }, { 1, 2 }, { 3, 2 }, { 1, 3 }, { 2, 3 }, { 3, 3 } }));
+    ASSERT_EQ(target.hullPoints(), std::vector<QPoint>({ { 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<QPointF>({ { 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<ShaderManager::Effect, double> 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 29/70] 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 30/70] 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<QPoint> &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<QPoint> &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<QPoint> &, 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<Costume>("", "", "png");
     std::string costumeData = readFileStr("image.png");
     costume->setData(costumeData.size(), static_cast<void *>(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 31/70] 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<QPoint> &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<QPoint> &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<CpuTextureManager> m_textureManager; // NOTE: Use textureManager()!
         std::unique_ptr<QOpenGLFunctions> m_glF;
         std::unordered_map<ShaderManager::Effect, double> 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 32/70] 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<QPoint> &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<QPoint> &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 33/70] 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<libscratchcpp::Sprite *> &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<libscratchcpp::Sprite *> &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 34/70] 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 35/70] 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<double>::infinity();
     double top = -std::numeric_limits<double>::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 36/70] 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 37/70] 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 38/70] 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 39/70] 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 40/70] 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<QPoint> &hullPoints() const = 0;
 
         virtual bool containsScratchPoint(double x, double y) const = 0;
+
+        virtual bool touchingClones(const std::vector<libscratchcpp::Sprite *> &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<libscratchcpp::Sprite *> &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<IRenderedTarget *> candidates;
+
+    // Calculate the union of the bounding rectangle intersections
+    for (auto clone : clones) {
+        Q_ASSERT(clone);
+
+        if (!clone)
+            continue;
+
+        SpriteModel *model = static_cast<SpriteModel *>(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<libscratchcpp::Sprite *> &) 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<libscratchcpp::Sprite *> &), (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 <scratchcpp/costume.h>
 #include <scratchcpp/rect.h>
 #include <enginemock.h>
+#include <renderedtargetmock.h>
 
 #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<Costume>("", "", "png");
+    std::string costumeData = readFileStr("image.png");
+    costume->setData(costumeData.size(), static_cast<void *>(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 41/70] 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 42/70] 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<libscratchcpp::Sprite *> &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<libscratchcpp::Sprite *> &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<Sprite *> clones = { &clone1, &clone2 };
+    std::vector<Sprite *> actualClones;
+
+    EXPECT_CALL(renderedTarget, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&actualClones](const std::vector<Sprite *> &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<Sprite *> &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<Sprite *> clones = { &clone1, &clone2 };
+    std::vector<Sprite *> actualClones;
+
+    EXPECT_CALL(renderedTarget, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&actualClones](const std::vector<Sprite *> &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<Sprite *> &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 43/70] 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<double>::infinity();
     double top = -std::numeric_limits<double>::infinity();
     double right = -std::numeric_limits<double>::infinity();
@@ -375,7 +377,7 @@ Rect RenderedTarget::getBounds() const
     const std::vector<QPoint> &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();
 

From 88f44d09087cef1ef3537c1c2b55a1c1ce7dbe20 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 30 Mar 2024 13:36:16 +0100
Subject: [PATCH 44/70] CpuTextureManager: Use QOpenGLFunctions for
 glReadPixels

---
 src/cputexturemanager.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/cputexturemanager.cpp b/src/cputexturemanager.cpp
index 963b0d1..d3d3b26 100644
--- a/src/cputexturemanager.cpp
+++ b/src/cputexturemanager.cpp
@@ -77,7 +77,7 @@ bool CpuTextureManager::addTexture(const Texture &texture)
 
     // Read pixels
     GLubyte *pixels = new GLubyte[width * height * 4]; // 4 channels (RGBA)
-    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
+    glF.glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
 
     // Flip vertically
     int rowSize = width * 4;

From f4aa357d1b774d263f4c7e318e485baeab2f2348 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sat, 30 Mar 2024 14:09:33 +0100
Subject: [PATCH 45/70] README: Mark touching sprite block as finished

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 17bc3ae..d1b9218 100644
--- a/README.md
+++ b/README.md
@@ -144,7 +144,7 @@ int main(int argc, char **argv) {
 - [x] Sprite click events
 - [x] Clones
 - [x] Sprite dragging
-- [ ] Touching sprite block
+- [x] Touching sprite block
 - [ ] Touching color blocks
 - [ ] Pen blocks (all blocks except the stamp block are implemented)
 - [x] Monitors

From b4cdd160693c059f9b7dc4a395d4fc95f6aa1d88 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 31 Mar 2024 12:33:07 +0200
Subject: [PATCH 46/70] Fix calculation of bounds

---
 src/renderedtarget.cpp                      | 27 ++++---
 test/renderedtarget/renderedtarget_test.cpp | 82 ++++++++++-----------
 2 files changed, 57 insertions(+), 52 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 6c33896..cac8bc4 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -362,10 +362,12 @@ Rect RenderedTarget::getBounds() const
     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_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 textureScale = m_skin->getTextureScale(m_cpuTexture);
+    const double bitmapRes = m_costume->bitmapResolution();
+    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 / bitmapRes - width / 2;
+    const double originY = -m_costume->rotationCenterY() * m_size / bitmapRes + height / 2;
     const double rot = -rotation() * pi / 180;
     const double sinRot = std::sin(rot);
     const double cosRot = std::cos(rot);
@@ -377,9 +379,11 @@ Rect RenderedTarget::getBounds() const
     const std::vector<QPoint> &points = hullPoints();
 
     for (const QPointF &point : points) {
-        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();
+        double x = point.x() * m_size / textureScale / bitmapRes - width / 2;
+        double y = height / 2 - point.y() * m_size / textureScale / bitmapRes;
+        const QPointF transformed = transformPoint(x, y, originX, originY, sinRot, cosRot);
+        x = transformed.x() * (m_mirrorHorizontally ? -1 : 1);
+        y = transformed.y();
 
         if (x < left)
             left = x;
@@ -422,10 +426,11 @@ Rect RenderedTarget::getFastBounds() const
         return Rect(m_x, m_y, m_x, m_y);
 
     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 / m_costume->bitmapResolution() - width / 2;
-    const double originY = -m_costume->rotationCenterY() * m_size / m_costume->bitmapResolution() + height / 2;
+    const double bitmapRes = m_costume->bitmapResolution();
+    const double width = m_cpuTexture.width() * m_size / textureScale / bitmapRes;
+    const double height = m_cpuTexture.height() * m_size / textureScale / bitmapRes;
+    const double originX = m_costume->rotationCenterX() * m_size / bitmapRes - width / 2;
+    const double originY = -m_costume->rotationCenterY() * m_size / bitmapRes + height / 2;
     const double rot = -rotation() * pi / 180;
     const double sinRot = std::sin(rot);
     const double cosRot = std::cos(rot);
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index a24edab..26af8a3 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -663,64 +663,64 @@ TEST_F(RenderedTargetTest, GetBounds)
     target.beforeRedraw();
 
     Rect bounds = target.getBounds();
-    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);
+    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);
 
     QRectF bubbleBounds = target.getBoundsForBubble();
-    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);
+    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);
 
     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, 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);
+    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);
 
     bubbleBounds = target.getBoundsForBubble();
-    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);
+    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);
 
     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, 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);
+    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);
 
     bubbleBounds = target.getBoundsForBubble();
-    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);
+    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);
 
     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, -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);
+    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);
 
     bubbleBounds = target.getBoundsForBubble();
-    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);
+    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);
 
     context.doneCurrent();
 }
@@ -759,9 +759,9 @@ TEST_F(RenderedTargetTest, GetFastBounds)
     target.beforeRedraw();
 
     Rect bounds = target.getFastBounds();
-    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.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.bottom() * 100) / 100, -125.4);
 
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
@@ -769,30 +769,30 @@ TEST_F(RenderedTargetTest, GetFastBounds)
     target.updateRotationStyle(Sprite::RotationStyle::LeftRight);
 
     bounds = target.getFastBounds();
-    ASSERT_EQ(std::round(bounds.left() * 100) / 100, 69.78);
+    ASSERT_EQ(std::round(bounds.left() * 100) / 100, 71.67);
     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, -114.34);
+    ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -111.51);
 
     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, 69.78);
+    ASSERT_EQ(std::round(bounds.left() * 100) / 100, 71.67);
     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, -114.34);
+    ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -111.51);
 
     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, -767);
+    ASSERT_EQ(std::round(bounds.left() * 100) / 100, -496.15);
     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, 737.38);
+    ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, 1143.65);
 
     context.doneCurrent();
 }

From e895cd3fbc42a410743abf2a6e2fd64a1d2b8fcf Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 31 Mar 2024 12:33:20 +0200
Subject: [PATCH 47/70] TextBubble: Fix stage bounds

---
 src/internal/TextBubble.qml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/internal/TextBubble.qml b/src/internal/TextBubble.qml
index f70df26..41e4ae7 100644
--- a/src/internal/TextBubble.qml
+++ b/src/internal/TextBubble.qml
@@ -44,7 +44,7 @@ TextBubbleShape {
             return;
 
         const targetBounds = target.getBoundsForBubble();
-        const stageBounds = Qt.rect(-root.stageWidth / 2, root.stageHeight / 2, root.stageWidth, root.stageHeight);
+        const stageBounds = Qt.rect(-root.stageWidth / 2, root.stageHeight / 2, root.stageWidth, -root.stageHeight);
 
         if (onSpriteRight && nativeWidth + targetBounds.right > stageBounds.right &&
             (targetBounds.left - nativeWidth > stageBounds.left)) { // Only flip if it would fit

From f17c44493524450046bde18323c375bb2a3dc881 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 1 Apr 2024 11:55:58 +0200
Subject: [PATCH 48/70] Update touchingClones() to be closer to Scratch

* clamp bounds to the stage
* snap bounds to int
---
 src/renderedtarget.cpp                      | 44 ++++++++++-
 src/renderedtarget.h                        |  2 +
 test/renderedtarget/renderedtarget_test.cpp | 84 +++++++++++++++++++--
 3 files changed, 120 insertions(+), 10 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index cac8bc4..70b84a5 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -605,8 +605,11 @@ bool RenderedTarget::touchingClones(const std::vector<libscratchcpp::Sprite *> &
 {
     // 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()));
+    const QRectF myRect = touchingBounds();
+
+    if (myRect.isEmpty())
+        return false;
+
     QRectF united;
     std::vector<IRenderedTarget *> candidates;
 
@@ -624,8 +627,9 @@ bool RenderedTarget::touchingClones(const std::vector<libscratchcpp::Sprite *> &
             // 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()));
+            Rect scratchRect = candidate->getFastBounds();
+            // TODO: Use Rect::snapToInt()
+            QRect rect(QPoint(scratchRect.left(), scratchRect.bottom()), QPoint(scratchRect.right(), scratchRect.top()));
             QRectF intersected = myRect.intersected(rect);
 
             // Add it to the union
@@ -791,6 +795,38 @@ CpuTextureManager *RenderedTarget::textureManager()
     return m_textureManager.get();
 }
 
+QRectF RenderedTarget::touchingBounds() const
+{
+    // https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L1330-L1350
+    if (!m_engine)
+        return QRectF();
+
+    Rect scratchBounds = getFastBounds();
+
+    // Limit queries to the stage size
+    const double stageWidth = m_engine->stageWidth();
+    const double stageHeight = m_engine->stageHeight();
+    clampRect(scratchBounds, -stageWidth / 2, stageWidth / 2, -stageHeight / 2, stageHeight / 2);
+
+    // TODO: Use Rect::snapToInt()
+    QRect bounds(QPoint(scratchBounds.left(), scratchBounds.bottom()), QPoint(scratchBounds.right(), scratchBounds.top()));
+    return bounds;
+}
+
+void RenderedTarget::clampRect(Rect &rect, double left, double right, double bottom, double top)
+{
+    // TODO: Use Rect::clamp()
+    rect.setLeft(std::max(rect.left(), left));
+    rect.setRight(std::min(rect.right(), right));
+    rect.setBottom(std::max(rect.bottom(), bottom));
+    rect.setTop(std::min(rect.top(), top));
+
+    rect.setLeft(std::min(rect.left(), right));
+    rect.setRight(std::max(rect.right(), left));
+    rect.setBottom(std::min(rect.bottom(), top));
+    rect.setTop(std::max(rect.top(), bottom));
+}
+
 bool RenderedTarget::mirrorHorizontally() const
 {
     return m_mirrorHorizontally;
diff --git a/src/renderedtarget.h b/src/renderedtarget.h
index 3b3d0c5..83e8faa 100644
--- a/src/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -122,6 +122,8 @@ class RenderedTarget : public IRenderedTarget
         QPointF transformPoint(double scratchX, double scratchY, double originX, double originY, double sinRot, double cosRot) const;
         QPointF mapFromStageWithOriginPoint(const QPointF &scenePoint) const;
         CpuTextureManager *textureManager();
+        QRectF touchingBounds() const;
+        static void clampRect(libscratchcpp::Rect &rect, double left, double right, double bottom, double top);
 
         libscratchcpp::IEngine *m_engine = nullptr;
         libscratchcpp::Costume *m_costume = nullptr;
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index 26af8a3..a33c79b 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -836,7 +836,7 @@ TEST_F(RenderedTargetTest, TouchingClones)
     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(target2, getFastBounds()).WillOnce(Return(Rect(-5, -1, 1, -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));
@@ -856,30 +856,46 @@ TEST_F(RenderedTargetTest, TouchingClones)
     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(target2, getFastBounds()).WillOnce(Return(Rect(-5, -1, 1, -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(target2, getFastBounds()).WillOnce(Return(Rect(-5, -1, 2, -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, -6.5, 1.8, -8)));
+    EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -6, 2, -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));
@@ -888,12 +904,68 @@ TEST_F(RenderedTargetTest, TouchingClones)
 
     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(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(true));
+    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, -6, 2, -8)));
+    EXPECT_CALL(target1, containsScratchPoint(1, -3)).WillOnce(Return(false));
+    EXPECT_CALL(target2, containsScratchPoint(1, -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(target2, getFastBounds()).WillOnce(Return(Rect(-5, -6, 2, -8)));
+    EXPECT_CALL(target1, containsScratchPoint).Times(0);
+    EXPECT_CALL(target2, containsScratchPoint).Times(0);
+    ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 }));
+
+    // Out of bounds: top left
+    target.updateX(-300);
+    target.updateY(200);
+    EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2 - 300, 1 + 200, 6 - 300, -5 + 200)));
+    EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5 - 300, -6.5 + 200, 1.8 - 300, -8 + 200)));
+    EXPECT_CALL(target1, containsScratchPoint).Times(0);
+    EXPECT_CALL(target2, containsScratchPoint).Times(0);
+    ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 }));
+
+    // Out of bounds: top right
+    target.updateX(300);
+    target.updateY(200);
+    EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2 + 300, 1 + 200, 6 + 300, -5 + 200)));
+    EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5 + 300, -6.5 + 200, 1.8 + 300, -8 + 200)));
+    EXPECT_CALL(target1, containsScratchPoint).Times(0);
+    EXPECT_CALL(target2, containsScratchPoint).Times(0);
+    ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 }));
+
+    // Out of bounds: bottom right
+    target.updateX(300);
+    target.updateY(-200);
+    EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2 + 300, 1 - 200, 6 + 300, -5 - 200)));
+    EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5 + 300, -6.5 - 200, 1.8 + 300, -8 - 200)));
+    EXPECT_CALL(target1, containsScratchPoint).Times(0);
+    EXPECT_CALL(target2, containsScratchPoint).Times(0);
+    ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 }));
+
+    // Out of bounds: bottom left
+    target.updateX(-300);
+    target.updateY(-200);
+    EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2 - 300, 1 - 200, 6 - 300, -5 - 200)));
+    EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5 - 300, -6.5 - 200, 1.8 - 300, -8 - 200)));
     EXPECT_CALL(target1, containsScratchPoint).Times(0);
     EXPECT_CALL(target2, containsScratchPoint).Times(0);
     ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 }));

From 2bb29e826f5924b03c1f7d24761af62827d7ef64 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Wed, 3 Apr 2024 16:14:06 +0200
Subject: [PATCH 49/70] Update libscratchcpp to latest master

---
 libscratchcpp       | 2 +-
 src/spritemodel.cpp | 5 +++++
 src/spritemodel.h   | 1 +
 src/stagemodel.cpp  | 5 +++++
 src/stagemodel.h    | 1 +
 5 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/libscratchcpp b/libscratchcpp
index 9a1d262..9ec931a 160000
--- a/libscratchcpp
+++ b/libscratchcpp
@@ -1 +1 @@
-Subproject commit 9a1d262bfcf778ddb2b24f339e9aa5ed95ba8ad0
+Subproject commit 9ec931a59daf4d7101382a6f91391b06c591badc
diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp
index 73eee92..2b6aba3 100644
--- a/src/spritemodel.cpp
+++ b/src/spritemodel.cpp
@@ -160,6 +160,11 @@ bool SpriteModel::touchingPoint(double x, double y) const
     return m_renderedTarget->containsScratchPoint(x, y);
 }
 
+bool SpriteModel::touchingColor(const libscratchcpp::Value &color) const
+{
+    return false;
+}
+
 libscratchcpp::Sprite *SpriteModel::sprite() const
 {
     return m_sprite;
diff --git a/src/spritemodel.h b/src/spritemodel.h
index bc8da74..ee4042c 100644
--- a/src/spritemodel.h
+++ b/src/spritemodel.h
@@ -59,6 +59,7 @@ class SpriteModel
 
         bool touchingClones(const std::vector<libscratchcpp::Sprite *> &clones) const override;
         bool touchingPoint(double x, double y) const override;
+        bool touchingColor(const libscratchcpp::Value &color) const override;
 
         libscratchcpp::Sprite *sprite() const;
 
diff --git a/src/stagemodel.cpp b/src/stagemodel.cpp
index d57a0af..039d875 100644
--- a/src/stagemodel.cpp
+++ b/src/stagemodel.cpp
@@ -97,6 +97,11 @@ bool StageModel::touchingPoint(double x, double y) const
     return m_renderedTarget->containsScratchPoint(x, y);
 }
 
+bool StageModel::touchingColor(const libscratchcpp::Value &color) const
+{
+    return false;
+}
+
 void StageModel::loadCostume()
 {
     if (m_renderedTarget && m_stage)
diff --git a/src/stagemodel.h b/src/stagemodel.h
index e70ef81..8f586c7 100644
--- a/src/stagemodel.h
+++ b/src/stagemodel.h
@@ -45,6 +45,7 @@ class StageModel
 
         bool touchingClones(const std::vector<libscratchcpp::Sprite *> &clones) const override;
         bool touchingPoint(double x, double y) const override;
+        bool touchingColor(const libscratchcpp::Value &color) const override;
 
         Q_INVOKABLE void loadCostume();
 

From 7692c154f22458ac6728c3a913ea9e7c2d1c753a Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 4 Apr 2024 09:45:07 +0200
Subject: [PATCH 50/70] ProjectLoader: Load projects if file name didn't change

---
 src/projectloader.cpp | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/src/projectloader.cpp b/src/projectloader.cpp
index cfe95be..6db2872 100644
--- a/src/projectloader.cpp
+++ b/src/projectloader.cpp
@@ -59,9 +59,6 @@ void ProjectLoader::setFileName(const QString &newFileName)
     if (m_loadThread.isRunning())
         m_loadThread.waitForFinished();
 
-    if (m_fileName == newFileName)
-        return;
-
     m_fileName = newFileName;
 
     // Stop the project

From 07bccf938d62139a3c6c2fc1947d2ead1181add6 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 4 Apr 2024 09:50:06 +0200
Subject: [PATCH 51/70] Add load() function to ProjectPlayer

---
 src/ProjectPlayer.qml | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml
index 79bf270..19a18fc 100644
--- a/src/ProjectPlayer.qml
+++ b/src/ProjectPlayer.qml
@@ -8,7 +8,7 @@ import ScratchCPP.Render
 import "internal"
 
 ProjectScene {
-    property string fileName
+	readonly property string fileName: loader.fileName
     property int stageWidth: 480
     property int stageHeight: 360
     property alias fps: loader.fps
@@ -28,6 +28,10 @@ ProjectScene {
     stageScale: (stageWidth == 0 || stageHeight == 0) ? 1 : Math.min(width / stageWidth, height / stageHeight)
     onFileNameChanged: priv.loading = true;
 
+	function load(fileName) {
+		loader.fileName = fileName;
+	}
+
     QtObject {
         id: priv
         property bool loading: false

From 81f19c796b784f973b8f202e7687f49a29d70035 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 4 Apr 2024 10:01:39 +0200
Subject: [PATCH 52/70] ProjectLoader: Ignore empty file names

---
 src/projectloader.cpp | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/projectloader.cpp b/src/projectloader.cpp
index 6db2872..c7a4984 100644
--- a/src/projectloader.cpp
+++ b/src/projectloader.cpp
@@ -59,6 +59,9 @@ void ProjectLoader::setFileName(const QString &newFileName)
     if (m_loadThread.isRunning())
         m_loadThread.waitForFinished();
 
+    if (newFileName.isEmpty())
+        return;
+
     m_fileName = newFileName;
 
     // Stop the project

From 351561f71e3a18ae03933a0615750e281cbdd2d2 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 4 Apr 2024 08:54:06 +0200
Subject: [PATCH 53/70] RenderedTarget: Move rect union calculation to a
 separate method

---
 src/renderedtarget.cpp | 106 +++++++++++++++++++++++++++++++----------
 src/renderedtarget.h   |   3 ++
 2 files changed, 84 insertions(+), 25 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 70b84a5..4e4143d 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -610,34 +610,10 @@ bool RenderedTarget::touchingClones(const std::vector<libscratchcpp::Sprite *> &
     if (myRect.isEmpty())
         return false;
 
-    QRectF united;
     std::vector<IRenderedTarget *> candidates;
 
     // Calculate the union of the bounding rectangle intersections
-    for (auto clone : clones) {
-        Q_ASSERT(clone);
-
-        if (!clone)
-            continue;
-
-        SpriteModel *model = static_cast<SpriteModel *>(clone->getInterface());
-        Q_ASSERT(model);
-
-        if (model) {
-            // Calculate the intersection of the bounding rectangles
-            IRenderedTarget *candidate = model->renderedTarget();
-            Q_ASSERT(candidate);
-            Rect scratchRect = candidate->getFastBounds();
-            // TODO: Use Rect::snapToInt()
-            QRect rect(QPoint(scratchRect.left(), scratchRect.bottom()), QPoint(scratchRect.right(), scratchRect.top()));
-            QRectF intersected = myRect.intersected(rect);
-
-            // Add it to the union
-            united = united.united(intersected);
-
-            candidates.push_back(candidate);
-        }
-    }
+    QRectF united = candidatesBounds(myRect, clones, candidates);
 
     if (united.isEmpty() || candidates.empty())
         return false;
@@ -813,6 +789,86 @@ QRectF RenderedTarget::touchingBounds() const
     return bounds;
 }
 
+QRectF RenderedTarget::candidatesBounds(const QRectF &targetRect, const std::vector<Target *> &candidates, std::vector<IRenderedTarget *> &dst)
+{
+    QRectF united;
+    dst.clear();
+
+    for (auto candidate : candidates) {
+        Q_ASSERT(candidate);
+
+        if (!candidate)
+            continue;
+
+        IRenderedTarget *target = nullptr;
+
+        if (candidate->isStage()) {
+            Stage *stage = static_cast<Stage *>(candidate);
+            StageModel *model = static_cast<StageModel *>(stage->getInterface());
+            Q_ASSERT(model);
+
+            if (model)
+                target = model->renderedTarget();
+        } else {
+            Sprite *sprite = static_cast<Sprite *>(candidate);
+            SpriteModel *model = static_cast<SpriteModel *>(sprite->getInterface());
+            Q_ASSERT(model);
+
+            if (model)
+                target = model->renderedTarget();
+        }
+
+        united = united.united(candidateIntersection(targetRect, target));
+
+        if (target)
+            dst.push_back(target);
+    }
+
+    return united;
+}
+
+QRectF RenderedTarget::candidatesBounds(const QRectF &targetRect, const std::vector<libscratchcpp::Sprite *> &candidates, std::vector<IRenderedTarget *> &dst)
+{
+    QRectF united;
+    dst.clear();
+
+    for (auto candidate : candidates) {
+        Q_ASSERT(candidate);
+
+        if (!candidate)
+            continue;
+
+        IRenderedTarget *target = nullptr;
+        SpriteModel *model = static_cast<SpriteModel *>(candidate->getInterface());
+        Q_ASSERT(model);
+
+        if (model)
+            target = model->renderedTarget();
+
+        united = united.united(candidateIntersection(targetRect, target));
+
+        if (target)
+            dst.push_back(target);
+    }
+
+    return united;
+}
+
+QRectF RenderedTarget::candidateIntersection(const QRectF &targetRect, IRenderedTarget *target)
+{
+    Q_ASSERT(target);
+
+    if (target) {
+        // Calculate the intersection of the bounding rectangles
+        Rect scratchRect = target->getFastBounds();
+        // TODO: Use Rect::snapToInt()
+        QRect rect(QPoint(scratchRect.left(), scratchRect.bottom()), QPoint(scratchRect.right(), scratchRect.top()));
+        return targetRect.intersected(rect);
+    }
+
+    return QRectF();
+}
+
 void RenderedTarget::clampRect(Rect &rect, double left, double right, double bottom, double top)
 {
     // TODO: Use Rect::clamp()
diff --git a/src/renderedtarget.h b/src/renderedtarget.h
index 83e8faa..4223de0 100644
--- a/src/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -123,6 +123,9 @@ class RenderedTarget : public IRenderedTarget
         QPointF mapFromStageWithOriginPoint(const QPointF &scenePoint) const;
         CpuTextureManager *textureManager();
         QRectF touchingBounds() const;
+        static QRectF candidatesBounds(const QRectF &targetRect, const std::vector<libscratchcpp::Target *> &candidates, std::vector<IRenderedTarget *> &dst);
+        static QRectF candidatesBounds(const QRectF &targetRect, const std::vector<libscratchcpp::Sprite *> &candidates, std::vector<IRenderedTarget *> &dst);
+        static QRectF candidateIntersection(const QRectF &targetRect, IRenderedTarget *target);
         static void clampRect(libscratchcpp::Rect &rect, double left, double right, double bottom, double top);
 
         libscratchcpp::IEngine *m_engine = nullptr;

From 10f535c6b9a15f8cd04d4a748acdfbcec3c48f91 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Fri, 5 Apr 2024 17:05:04 +0200
Subject: [PATCH 54/70] RenderedTarget: Optimize containsScratchPoint()

---
 src/renderedtarget.cpp                      | 59 +++++++++++++--------
 src/renderedtarget.h                        |  2 +
 test/renderedtarget/renderedtarget_test.cpp |  8 +--
 3 files changed, 41 insertions(+), 28 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 4e4143d..96a9566 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -571,34 +571,15 @@ bool RenderedTarget::contains(const QPointF &point) const
     translatedPoint = mapFromStageWithOriginPoint(translatedPoint);
     translatedPoint /= scaleRatio;
 
-    if (!boundingRect().contains(translatedPoint))
-        return false;
-
-    const std::vector<QPoint> &points = hullPoints();
-    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()) {
-        // The point is beyond the last point in the convex hull
-        return false;
-    }
-
-    // Check if the point is equal to the one found
-    return *it == intPoint;
+    return containsLocalPoint(translatedPoint);
 }
 
 bool RenderedTarget::containsScratchPoint(double x, double y) const
 {
-    if (!m_engine || !parentItem())
+    if (!m_engine)
         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)));
+    return containsLocalPoint(mapFromScratchToLocal(QPointF(x, y)));
 }
 
 bool RenderedTarget::touchingClones(const std::vector<libscratchcpp::Sprite *> &clones) const
@@ -734,6 +715,24 @@ void RenderedTarget::updateHullPoints()
     // TODO: Apply graphic effects (#117)
 }
 
+bool RenderedTarget::containsLocalPoint(const QPointF &point) const
+{
+    if (!boundingRect().contains(point))
+        return false;
+
+    const std::vector<QPoint> &points = hullPoints();
+    QPoint intPoint = point.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()) {
+        // The point is beyond the last point in the convex hull
+        return false;
+    }
+
+    // Check if the point is equal to the one found
+    return *it == intPoint;
+}
+
 QPointF RenderedTarget::transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const
 {
     return transformPoint(scratchX, scratchY, originX, originY, std::sin(rot), std::cos(rot));
@@ -763,6 +762,22 @@ QPointF RenderedTarget::mapFromStageWithOriginPoint(const QPointF &scenePoint) c
     return localPoint;
 }
 
+QPointF RenderedTarget::mapFromScratchToLocal(const QPointF &point) const
+{
+    QTransform t;
+    const double textureScale = m_skin->getTextureScale(m_cpuTexture);
+    const double scale = m_size / textureScale;
+    const double mirror = m_mirrorHorizontally ? -1 : 1;
+    const double bitmapRes = m_costume->bitmapResolution();
+    t.translate(m_costume->rotationCenterX() * textureScale, m_costume->rotationCenterY() * textureScale);
+    t.rotate(-rotation());
+    t.scale(bitmapRes * mirror / scale, -bitmapRes / scale);
+    t.translate(-m_x, -m_y);
+
+    QPointF localPoint = t.map(point);
+    return localPoint;
+}
+
 CpuTextureManager *RenderedTarget::textureManager()
 {
     if (!m_textureManager)
diff --git a/src/renderedtarget.h b/src/renderedtarget.h
index 4223de0..616ebbd 100644
--- a/src/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -118,9 +118,11 @@ class RenderedTarget : public IRenderedTarget
         void handleSceneMouseMove(qreal x, qreal y);
         bool convexHullPointsNeeded() const;
         void updateHullPoints();
+        bool containsLocalPoint(const QPointF &point) const;
         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;
+        QPointF mapFromScratchToLocal(const QPointF &point) const;
         CpuTextureManager *textureManager();
         QRectF touchingBounds() const;
         static QRectF candidatesBounds(const QRectF &targetRect, const std::vector<libscratchcpp::Target *> &candidates, std::vector<IRenderedTarget *> &dst);
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index a33c79b..73d7fbc 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -328,8 +328,8 @@ TEST_F(RenderedTargetTest, HullPoints)
     target.loadCostumes();
     target.updateCostume(costume.get());
     target.setStageScale(2);
-    target.setX(25);
-    target.setY(30);
+    target.updateX(-227.5);
+    target.updateY(165);
 
     // Test hull points
     target.setWidth(3);
@@ -361,8 +361,6 @@ TEST_F(RenderedTargetTest, HullPoints)
     // 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 }));
@@ -373,8 +371,6 @@ TEST_F(RenderedTargetTest, HullPoints)
 
     // 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 2ce28bdb06688b2023f33a400fe6dc1ee0e0a678 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Fri, 5 Apr 2024 20:53:12 +0200
Subject: [PATCH 55/70] RenderedTarget: Exclude the target from touching
 candidates

---
 src/renderedtarget.cpp | 16 ++++++++++------
 src/renderedtarget.h   |  4 ++--
 2 files changed, 12 insertions(+), 8 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 96a9566..71af881 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -804,7 +804,7 @@ QRectF RenderedTarget::touchingBounds() const
     return bounds;
 }
 
-QRectF RenderedTarget::candidatesBounds(const QRectF &targetRect, const std::vector<Target *> &candidates, std::vector<IRenderedTarget *> &dst)
+QRectF RenderedTarget::candidatesBounds(const QRectF &targetRect, const std::vector<Target *> &candidates, std::vector<IRenderedTarget *> &dst) const
 {
     QRectF united;
     dst.clear();
@@ -833,16 +833,18 @@ QRectF RenderedTarget::candidatesBounds(const QRectF &targetRect, const std::vec
                 target = model->renderedTarget();
         }
 
-        united = united.united(candidateIntersection(targetRect, target));
+        Q_ASSERT(target);
 
-        if (target)
+        if (target && target != this) {
+            united = united.united(candidateIntersection(targetRect, target));
             dst.push_back(target);
+        }
     }
 
     return united;
 }
 
-QRectF RenderedTarget::candidatesBounds(const QRectF &targetRect, const std::vector<libscratchcpp::Sprite *> &candidates, std::vector<IRenderedTarget *> &dst)
+QRectF RenderedTarget::candidatesBounds(const QRectF &targetRect, const std::vector<libscratchcpp::Sprite *> &candidates, std::vector<IRenderedTarget *> &dst) const
 {
     QRectF united;
     dst.clear();
@@ -860,10 +862,12 @@ QRectF RenderedTarget::candidatesBounds(const QRectF &targetRect, const std::vec
         if (model)
             target = model->renderedTarget();
 
-        united = united.united(candidateIntersection(targetRect, target));
+        Q_ASSERT(target);
 
-        if (target)
+        if (target && target != this) {
+            united = united.united(candidateIntersection(targetRect, target));
             dst.push_back(target);
+        }
     }
 
     return united;
diff --git a/src/renderedtarget.h b/src/renderedtarget.h
index 616ebbd..16e6268 100644
--- a/src/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -125,8 +125,8 @@ class RenderedTarget : public IRenderedTarget
         QPointF mapFromScratchToLocal(const QPointF &point) const;
         CpuTextureManager *textureManager();
         QRectF touchingBounds() const;
-        static QRectF candidatesBounds(const QRectF &targetRect, const std::vector<libscratchcpp::Target *> &candidates, std::vector<IRenderedTarget *> &dst);
-        static QRectF candidatesBounds(const QRectF &targetRect, const std::vector<libscratchcpp::Sprite *> &candidates, std::vector<IRenderedTarget *> &dst);
+        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;
         static QRectF candidateIntersection(const QRectF &targetRect, IRenderedTarget *target);
         static void clampRect(libscratchcpp::Rect &rect, double left, double right, double bottom, double top);
 

From ac18bcab50bb73da0eac49604cc6400e8a184dbd Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Fri, 5 Apr 2024 20:54:29 +0200
Subject: [PATCH 56/70] RenderedTarget: Declare textureManager() with const

---
 src/renderedtarget.cpp | 2 +-
 src/renderedtarget.h   | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 71af881..577220e 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -778,7 +778,7 @@ QPointF RenderedTarget::mapFromScratchToLocal(const QPointF &point) const
     return localPoint;
 }
 
-CpuTextureManager *RenderedTarget::textureManager()
+CpuTextureManager *RenderedTarget::textureManager() const
 {
     if (!m_textureManager)
         m_textureManager = std::make_shared<CpuTextureManager>();
diff --git a/src/renderedtarget.h b/src/renderedtarget.h
index 16e6268..f32419e 100644
--- a/src/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -123,7 +123,7 @@ class RenderedTarget : public IRenderedTarget
         QPointF transformPoint(double scratchX, double scratchY, double originX, double originY, double sinRot, double cosRot) const;
         QPointF mapFromStageWithOriginPoint(const QPointF &scenePoint) const;
         QPointF mapFromScratchToLocal(const QPointF &point) const;
-        CpuTextureManager *textureManager();
+        CpuTextureManager *textureManager() 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;
@@ -141,8 +141,8 @@ class RenderedTarget : public IRenderedTarget
         Skin *m_skin = nullptr;
         Texture m_texture;
         Texture m_oldTexture;
-        Texture m_cpuTexture;                                // without stage scale
-        std::shared_ptr<CpuTextureManager> m_textureManager; // NOTE: Use textureManager()!
+        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;
         double m_size = 1;

From 7b3d3a30c81b85e50753fdf4cacc8ef3bf331861 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 7 Apr 2024 19:13:17 +0200
Subject: [PATCH 57/70] Add colorAtScratchPoint() method

---
 src/irenderedtarget.h                       |  1 +
 src/renderedtarget.cpp                      | 24 +++++++++++++++++++++
 src/renderedtarget.h                        |  1 +
 test/mocks/renderedtargetmock.h             |  1 +
 test/renderedtarget/renderedtarget_test.cpp | 24 ++++++++++++++++++++-
 5 files changed, 50 insertions(+), 1 deletion(-)

diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h
index 1a5f267..af9d075 100644
--- a/src/irenderedtarget.h
+++ b/src/irenderedtarget.h
@@ -86,6 +86,7 @@ class IRenderedTarget : public QNanoQuickItem
         virtual const std::vector<QPoint> &hullPoints() const = 0;
 
         virtual bool containsScratchPoint(double x, double y) const = 0;
+        virtual QRgb colorAtScratchPoint(double x, double y) const = 0;
 
         virtual bool touchingClones(const std::vector<libscratchcpp::Sprite *> &clones) const = 0;
 };
diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 577220e..664cd0d 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -582,6 +582,30 @@ bool RenderedTarget::containsScratchPoint(double x, double y) const
     return containsLocalPoint(mapFromScratchToLocal(QPointF(x, y)));
 }
 
+QRgb RenderedTarget::colorAtScratchPoint(double x, double y) const
+{
+    if (!m_engine || !m_cpuTexture.isValid())
+        return qRgba(0, 0, 0, 0);
+
+    // Translate the coordinates
+    QPointF point = mapFromScratchToLocal(QPointF(x, y));
+    x = point.x();
+    y = point.y();
+
+    const double width = m_cpuTexture.width();
+    const double height = m_cpuTexture.height();
+
+    // If the point is outside the texture, return fully transparent color
+    if ((x < 0 || x >= width) || (y < 0 || y >= height))
+        return qRgba(0, 0, 0, 0);
+
+    GLubyte *data = textureManager()->getTextureData(m_cpuTexture);
+    const int index = (y * width + x) * 4; // RGBA channels
+    Q_ASSERT(index >= 0 && index < width * height * 4);
+    // TODO: Apply graphic effects (#117)
+    return qRgba(data[index], data[index + 1], data[index + 2], data[index + 3]);
+}
+
 bool RenderedTarget::touchingClones(const std::vector<libscratchcpp::Sprite *> &clones) const
 {
     // https://github.com/scratchfoundation/scratch-render/blob/941562438fe3dd6e7d98d9387607d535dcd68d24/src/RenderWebGL.js#L967-L1002
diff --git a/src/renderedtarget.h b/src/renderedtarget.h
index f32419e..e30a2b0 100644
--- a/src/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -94,6 +94,7 @@ class RenderedTarget : public IRenderedTarget
 
         Q_INVOKABLE bool contains(const QPointF &point) const override;
         bool containsScratchPoint(double x, double y) const override;
+        QRgb colorAtScratchPoint(double x, double y) const override;
 
         bool touchingClones(const std::vector<libscratchcpp::Sprite *> &) const override;
 
diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h
index ee2974e..22c019e 100644
--- a/test/mocks/renderedtargetmock.h
+++ b/test/mocks/renderedtargetmock.h
@@ -71,6 +71,7 @@ class RenderedTargetMock : public IRenderedTarget
 
         MOCK_METHOD(bool, contains, (const QPointF &), (const, override));
         MOCK_METHOD(bool, containsScratchPoint, (double, double), (const, override));
+        MOCK_METHOD(QRgb, colorAtScratchPoint, (double, double), (const, override));
 
         MOCK_METHOD(bool, touchingClones, (const std::vector<libscratchcpp::Sprite *> &), (const, override));
 
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index 73d7fbc..d779b49 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -299,7 +299,7 @@ TEST_F(RenderedTargetTest, DeinitClone)
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
 }
 
-TEST_F(RenderedTargetTest, HullPoints)
+TEST_F(RenderedTargetTest, CpuRendering)
 {
     EngineMock engine;
     Sprite sprite;
@@ -392,6 +392,28 @@ TEST_F(RenderedTargetTest, HullPoints)
     ASSERT_TRUE(target.containsScratchPoint(-224.5, 162));    // [3, 3]
     ASSERT_FALSE(target.containsScratchPoint(-224.2, 161.5)); // [3.3, 3.5]
 
+    // Test colorAtScratchPoint()
+    ASSERT_EQ(target.colorAtScratchPoint(-227.5, 165), 0); // [0, 0]
+    ASSERT_EQ(target.colorAtScratchPoint(-226.5, 165), 0); // [1, 0]
+    ASSERT_EQ(target.colorAtScratchPoint(-225.5, 165), 0); // [2, 0]
+    ASSERT_EQ(target.colorAtScratchPoint(-224.5, 165), 0); // [3, 0]
+
+    ASSERT_EQ(target.colorAtScratchPoint(-227.5, 164), 0);             // [0, 1]
+    ASSERT_EQ(target.colorAtScratchPoint(-226.5, 164), 4278190335);    // [1, 1]
+    ASSERT_EQ(target.colorAtScratchPoint(-226.1, 163.75), 4278255615); // [1.4, 1.25]
+    ASSERT_EQ(target.colorAtScratchPoint(-225.5, 164), 4294902015);    // [2, 1]
+    ASSERT_EQ(target.colorAtScratchPoint(-224.5, 164), 4294934656);    // [3, 1]
+
+    ASSERT_EQ(target.colorAtScratchPoint(-226.5, 163), 4278190208); // [1, 2]
+    ASSERT_EQ(target.colorAtScratchPoint(-225.5, 163), 0);          // [2, 2]
+    ASSERT_EQ(target.colorAtScratchPoint(-224.5, 163), 2505545047); // [3, 2]
+    ASSERT_EQ(target.colorAtScratchPoint(-224, 162.9), 9764864);    // [3.5, 2.1]
+
+    ASSERT_EQ(target.colorAtScratchPoint(-226.5, 162), 4286578816); // [1, 3]
+    ASSERT_EQ(target.colorAtScratchPoint(-225.5, 162), 4286611711); // [2, 3]
+    ASSERT_EQ(target.colorAtScratchPoint(-224.5, 162), 4286611456); // [3, 3]
+    ASSERT_EQ(target.colorAtScratchPoint(-224.2, 161.5), 0);        // [3.3, 3.5]
+
     // Cleanup
     context.doneCurrent();
 }

From cc2c3c734dcb33854531f7ac45fb10bc4b412f21 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 7 Apr 2024 19:14:18 +0200
Subject: [PATCH 58/70] Add touchingColor() method

---
 src/irenderedtarget.h                       |   1 +
 src/renderedtarget.cpp                      | 132 +++++++++++++
 src/renderedtarget.h                        |   5 +
 test/mocks/renderedtargetmock.h             |   1 +
 test/renderedtarget/renderedtarget_test.cpp | 193 ++++++++++++++++++++
 5 files changed, 332 insertions(+)

diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h
index af9d075..59b8106 100644
--- a/src/irenderedtarget.h
+++ b/src/irenderedtarget.h
@@ -89,6 +89,7 @@ class IRenderedTarget : public QNanoQuickItem
         virtual QRgb colorAtScratchPoint(double x, double y) const = 0;
 
         virtual bool touchingClones(const std::vector<libscratchcpp::Sprite *> &clones) const = 0;
+        virtual bool touchingColor(const libscratchcpp::Value &color) const = 0;
 };
 
 } // namespace scratchcpprender
diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 664cd0d..c22ff3d 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -3,6 +3,7 @@
 #include <scratchcpp/iengine.h>
 #include <scratchcpp/costume.h>
 #include <scratchcpp/rect.h>
+#include <scratchcpp/value.h>
 #include <QtSvg/QSvgRenderer>
 #include <qnanopainter.h>
 
@@ -584,6 +585,7 @@ bool RenderedTarget::containsScratchPoint(double x, double y) const
 
 QRgb RenderedTarget::colorAtScratchPoint(double x, double y) const
 {
+    // NOTE: Only this target is processed! Use sampleColor3b() to get the final color.
     if (!m_engine || !m_cpuTexture.isValid())
         return qRgba(0, 0, 0, 0);
 
@@ -638,6 +640,48 @@ bool RenderedTarget::touchingClones(const std::vector<libscratchcpp::Sprite *> &
     return false;
 }
 
+bool RenderedTarget::touchingColor(const Value &color) const
+{
+    // https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L775-L841
+    QRgb rgb = convertColor(color);
+
+    std::vector<Target *> targets;
+    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;
+    }
+
+    QPointF point;
+
+    // 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)) {
+                point.setX(x);
+                point.setY(y);
+                QRgb pixelColor = sampleColor3b(point, candidates);
+
+                if (colorMatches(rgb, pixelColor))
+                    return true;
+            }
+        }
+    }
+
+    return false;
+}
+
 void RenderedTarget::calculatePos()
 {
     if (!m_skin || !m_costume || !m_engine)
@@ -810,6 +854,38 @@ CpuTextureManager *RenderedTarget::textureManager() const
     return m_textureManager.get();
 }
 
+void RenderedTarget::getVisibleTargets(std::vector<Target *> &dst) const
+{
+    dst.clear();
+
+    if (!m_engine)
+        return;
+
+    const auto &targets = m_engine->targets();
+
+    for (auto target : targets) {
+        Q_ASSERT(target);
+
+        if (target->isStage())
+            dst.push_back(target.get());
+        else {
+            Sprite *sprite = static_cast<Sprite *>(target.get());
+
+            if (sprite->visible())
+                dst.push_back(target.get());
+
+            const auto &clones = sprite->clones();
+
+            for (auto clone : clones) {
+                if (clone->visible())
+                    dst.push_back(clone.get());
+            }
+        }
+    }
+
+    std::sort(dst.begin(), dst.end(), [](Target *t1, Target *t2) { return t1->layerOrder() > t2->layerOrder(); });
+}
+
 QRectF RenderedTarget::touchingBounds() const
 {
     // https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L1330-L1350
@@ -926,6 +1002,62 @@ void RenderedTarget::clampRect(Rect &rect, double left, double right, double bot
     rect.setTop(std::max(rect.top(), bottom));
 }
 
+QRgb RenderedTarget::convertColor(const libscratchcpp::Value &color)
+{
+    // TODO: Remove this after libscratchcpp starts converting colors (it still needs to be converted to RGB here)
+    std::string stringValue;
+
+    if (color.isString())
+        stringValue = color.toString();
+
+    if (!stringValue.empty() && stringValue[0] == '#') {
+        bool valid = false;
+        QColor color;
+
+        if (stringValue.size() <= 7) // #RRGGBB
+        {
+            color = QColor::fromString(stringValue);
+            valid = color.isValid();
+        }
+
+        if (!valid)
+            color = Qt::black;
+
+        return color.rgb();
+
+    } else
+        return QColor::fromRgba(static_cast<QRgb>(color.toLong())).rgb();
+}
+
+bool RenderedTarget::colorMatches(QRgb a, QRgb b)
+{
+    // https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L77-L81
+    return (qRed(a) & 0b11111000) == (qRed(b) & 0b11111000) && (qGreen(a) & 0b11111000) == (qGreen(b) & 0b11111000) && (qBlue(a) & 0b11110000) == (qBlue(b) & 0b11110000);
+}
+
+QRgb RenderedTarget::sampleColor3b(const QPointF &point, const std::vector<IRenderedTarget *> &targets)
+{
+    // https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L1966-L1990
+    double blendAlpha = 1;
+    QRgb blendColor;
+    int r = 0, g = 0, b = 0;
+
+    for (int i = 0; blendAlpha != 0 && i < targets.size(); i++) {
+        Q_ASSERT(targets[i]);
+        blendColor = targets[i]->colorAtScratchPoint(point.x(), point.y());
+
+        r += qRed(blendColor) * blendAlpha;
+        g += qGreen(blendColor) * blendAlpha;
+        b += qBlue(blendColor) * blendAlpha;
+        blendAlpha *= (1.0 - (qAlpha(blendColor) / 255.0));
+    }
+
+    r += blendAlpha * 255;
+    g += blendAlpha * 255;
+    b += blendAlpha * 255;
+    return qRgb(r, g, b);
+}
+
 bool RenderedTarget::mirrorHorizontally() const
 {
     return m_mirrorHorizontally;
diff --git a/src/renderedtarget.h b/src/renderedtarget.h
index e30a2b0..8c1a28e 100644
--- a/src/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -97,6 +97,7 @@ class RenderedTarget : public IRenderedTarget
         QRgb colorAtScratchPoint(double x, double y) const override;
 
         bool touchingClones(const std::vector<libscratchcpp::Sprite *> &) const override;
+        bool touchingColor(const libscratchcpp::Value &color) const override;
 
     signals:
         void engineChanged();
@@ -125,11 +126,15 @@ class RenderedTarget : public IRenderedTarget
         QPointF mapFromStageWithOriginPoint(const QPointF &scenePoint) const;
         QPointF mapFromScratchToLocal(const QPointF &point) const;
         CpuTextureManager *textureManager() const;
+        void getVisibleTargets(std::vector<libscratchcpp::Target *> &dst) 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;
         static QRectF candidateIntersection(const QRectF &targetRect, IRenderedTarget *target);
         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 QRgb sampleColor3b(const QPointF &point, const std::vector<IRenderedTarget *> &targets);
 
         libscratchcpp::IEngine *m_engine = nullptr;
         libscratchcpp::Costume *m_costume = nullptr;
diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h
index 22c019e..6bfc154 100644
--- a/test/mocks/renderedtargetmock.h
+++ b/test/mocks/renderedtargetmock.h
@@ -74,6 +74,7 @@ class RenderedTargetMock : public IRenderedTarget
         MOCK_METHOD(QRgb, colorAtScratchPoint, (double, double), (const, override));
 
         MOCK_METHOD(bool, touchingClones, (const std::vector<libscratchcpp::Sprite *> &), (const, override));
+        MOCK_METHOD(bool, touchingColor, (const libscratchcpp::Value &), (const, override));
 
         MOCK_METHOD(QNanoQuickItemPainter *, createItemPainter, (), (const, override));
         MOCK_METHOD(void, hoverEnterEvent, (QHoverEvent *), (override));
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index d779b49..c9bbdb3 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -10,6 +10,7 @@
 #include <scratchcpp/sprite.h>
 #include <scratchcpp/costume.h>
 #include <scratchcpp/rect.h>
+#include <scratchcpp/value.h>
 #include <enginemock.h>
 #include <renderedtargetmock.h>
 
@@ -19,6 +20,7 @@ using namespace scratchcpprender;
 using namespace libscratchcpp;
 
 using ::testing::Return;
+using ::testing::ReturnRef;
 
 class RenderedTargetTest : public testing::Test
 {
@@ -991,3 +993,194 @@ TEST_F(RenderedTargetTest, TouchingClones)
     // Cleanup
     context.doneCurrent();
 }
+
+TEST_F(RenderedTargetTest, TouchingColor)
+{
+    EngineMock engine;
+    auto stage = std::make_shared<Stage>();
+    auto sprite = std::make_shared<Sprite>();
+    auto sprite1 = std::make_shared<Sprite>();
+    sprite1->setEngine(&engine);
+    EXPECT_CALL(engine, cloneLimit()).WillOnce(Return(-1));
+    EXPECT_CALL(engine, initClone);
+    EXPECT_CALL(engine, requestRedraw);
+    EXPECT_CALL(engine, moveSpriteBehindOther);
+    auto sprite2 = sprite1->clone();
+    StageModel stageModel;
+    SpriteModel model, model1, model2;
+    stage->setInterface(&stageModel);
+    sprite->setInterface(&model);
+    sprite1->setInterface(&model1);
+    sprite2->setInterface(&model2);
+
+    sprite->setLayerOrder(1);
+    sprite1->setLayerOrder(2);
+    sprite2->setLayerOrder(3);
+
+    const std::vector<std::shared_ptr<Target>> targets = { stage, sprite, sprite1 };
+
+    QQuickItem parent;
+    parent.setWidth(480);
+    parent.setHeight(360);
+
+    RenderedTarget target(&parent);
+    target.setEngine(&engine);
+    target.setSpriteModel(&model);
+    model.setRenderedTarget(&target);
+
+    RenderedTargetMock stageTarget, target1, target2;
+    stageModel.setRenderedTarget(&stageTarget);
+    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<Costume>("", "", "png");
+    std::string costumeData = readFileStr("image.png");
+    costume->setData(costumeData.size(), static_cast<void *>(costumeData.data()));
+    sprite->addCostume(costume);
+    target.loadCostumes();
+    target.updateCostume(costume.get());
+    target.beforeRedraw();
+
+    EXPECT_CALL(engine, targets()).WillRepeatedly(ReturnRef(targets));
+
+    static const Value color1 = 4286611711; // "purple"
+    static const Value color2 = 596083443;  // close to color1 and transparent
+    static const Value color3 = "#808000";  // "olive" (4286611456)
+    static const QRgb color3Int = 4286611456;
+    static const Value color4 = 2505545047; // transparent "hippie green"
+    static const Value color5 = 4287417025; // color1 + color4
+
+    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, -1, 1, -8)));
+    EXPECT_CALL(target2, colorAtScratchPoint(1, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(2, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(3, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(1, -2)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(3, -2)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(1, -1)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(2, -1)).WillOnce(Return(color4.toInt()));
+    EXPECT_CALL(target1, colorAtScratchPoint(2, -1)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(3, -1)).WillOnce(Return(color4.toInt()));
+    EXPECT_CALL(target1, colorAtScratchPoint(3, -1)).WillOnce(Return(color4.toInt()));
+    EXPECT_CALL(stageTarget, colorAtScratchPoint(3, -1)).WillOnce(Return(color4.toInt()));
+    ASSERT_FALSE(target.touchingColor(color1));
+
+    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, -1, 1, -8)));
+    EXPECT_CALL(target2, colorAtScratchPoint(1, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(2, -3)).WillOnce(Return(color1.toInt()));
+    ASSERT_TRUE(target.touchingColor(color1));
+
+    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, -1, 2, -8)));
+    EXPECT_CALL(target2, colorAtScratchPoint(1, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(2, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(3, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(1, -2)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(3, -2)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(1, -1)).WillOnce(Return(color4.toInt()));
+    EXPECT_CALL(target1, colorAtScratchPoint(1, -1)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(2, -1)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(3, -1)).WillOnce(Return(color3Int));
+    ASSERT_FALSE(target.touchingColor(color1));
+
+    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(1, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(2, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(3, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(1, -2)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(3, -2)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(1, -1)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(2, -1)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(3, -1)).WillOnce(Return(color1.toInt()));
+    ASSERT_TRUE(target.touchingColor(color2));
+
+    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.5, 1.8, -8)));
+    EXPECT_CALL(target2, colorAtScratchPoint(1, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(2, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(3, -3)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(1, -2)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(3, -2)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(1, -1)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(2, -1)).WillOnce(Return(color3Int));
+    EXPECT_CALL(target2, colorAtScratchPoint(3, -1)).WillOnce(Return(color4.toInt()));
+    EXPECT_CALL(target1, colorAtScratchPoint(3, -1)).WillOnce(Return(color1.toInt()));
+    ASSERT_FALSE(target.touchingColor(color1));
+
+    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(1, -3)).WillOnce(Return(color4.toInt()));
+    EXPECT_CALL(target1, colorAtScratchPoint(1, -3)).WillOnce(Return(color1.toInt()));
+    ASSERT_TRUE(target.touchingColor(color5));
+
+    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(stageTarget, colorAtScratchPoint).Times(0);
+    ASSERT_FALSE(target.touchingColor(color3));
+
+    // Out of bounds: top left
+    target.updateX(-300);
+    target.updateY(200);
+    EXPECT_CALL(stageTarget, getFastBounds()).WillOnce(Return(Rect(2 - 300, 1 + 200, 6 - 300, -5 + 200)));
+    EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2 - 300, 1 + 200, 6 - 300, -5 + 200)));
+    EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5 - 300, -6.5 + 200, 1.8 - 300, -8 + 200)));
+    EXPECT_CALL(target2, colorAtScratchPoint).Times(0);
+    EXPECT_CALL(target1, colorAtScratchPoint).Times(0);
+    EXPECT_CALL(stageTarget, colorAtScratchPoint).Times(0);
+    ASSERT_FALSE(target.touchingColor(color1));
+
+    // Out of bounds: top right
+    target.updateX(300);
+    target.updateY(200);
+    EXPECT_CALL(stageTarget, getFastBounds()).WillOnce(Return(Rect(2 + 300, 1 + 200, 6 + 300, -5 + 200)));
+    EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2 + 300, 1 + 200, 6 + 300, -5 + 200)));
+    EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5 + 300, -6.5 + 200, 1.8 + 300, -8 + 200)));
+    EXPECT_CALL(target2, colorAtScratchPoint).Times(0);
+    EXPECT_CALL(target1, colorAtScratchPoint).Times(0);
+    EXPECT_CALL(stageTarget, colorAtScratchPoint).Times(0);
+    ASSERT_FALSE(target.touchingColor(color1));
+
+    // Out of bounds: bottom right
+    target.updateX(300);
+    target.updateY(-200);
+    EXPECT_CALL(stageTarget, getFastBounds()).WillOnce(Return(Rect(2 + 300, 1 - 200, 6 + 300, -5 - 200)));
+    EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2 + 300, 1 - 200, 6 + 300, -5 - 200)));
+    EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5 + 300, -6.5 - 200, 1.8 + 300, -8 - 200)));
+    EXPECT_CALL(target2, colorAtScratchPoint).Times(0);
+    EXPECT_CALL(target1, colorAtScratchPoint).Times(0);
+    EXPECT_CALL(stageTarget, colorAtScratchPoint).Times(0);
+    ASSERT_FALSE(target.touchingColor(color1));
+
+    // Out of bounds: bottom left
+    target.updateX(-300);
+    target.updateY(-200);
+    EXPECT_CALL(stageTarget, getFastBounds()).WillOnce(Return(Rect(2 - 300, 1 - 200, 6 - 300, -5 - 200)));
+    EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2 - 300, 1 - 200, 6 - 300, -5 - 200)));
+    EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5 - 300, -6.5 - 200, 1.8 - 300, -8 - 200)));
+    EXPECT_CALL(target2, colorAtScratchPoint).Times(0);
+    EXPECT_CALL(target1, colorAtScratchPoint).Times(0);
+    EXPECT_CALL(stageTarget, colorAtScratchPoint).Times(0);
+    ASSERT_FALSE(target.touchingColor(color1));
+
+    // Cleanup
+    context.doneCurrent();
+}

From 476f02e01052e17d53563e0f14b8a2c4c9247558 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 7 Apr 2024 19:18:19 +0200
Subject: [PATCH 59/70] Implement touching color methods

---
 src/spritemodel.cpp                     |  2 +-
 src/stagemodel.cpp                      |  2 +-
 test/target_models/spritemodel_test.cpp | 16 ++++++++++++++++
 test/target_models/stagemodel_test.cpp  | 16 ++++++++++++++++
 4 files changed, 34 insertions(+), 2 deletions(-)

diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp
index 2b6aba3..0fe5b98 100644
--- a/src/spritemodel.cpp
+++ b/src/spritemodel.cpp
@@ -162,7 +162,7 @@ bool SpriteModel::touchingPoint(double x, double y) const
 
 bool SpriteModel::touchingColor(const libscratchcpp::Value &color) const
 {
-    return false;
+    return m_renderedTarget->touchingColor(color);
 }
 
 libscratchcpp::Sprite *SpriteModel::sprite() const
diff --git a/src/stagemodel.cpp b/src/stagemodel.cpp
index 039d875..50f834a 100644
--- a/src/stagemodel.cpp
+++ b/src/stagemodel.cpp
@@ -99,7 +99,7 @@ bool StageModel::touchingPoint(double x, double y) const
 
 bool StageModel::touchingColor(const libscratchcpp::Value &color) const
 {
-    return false;
+    return m_renderedTarget->touchingColor(color);
 }
 
 void StageModel::loadCostume()
diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp
index 6f44f88..8e81e29 100644
--- a/test/target_models/spritemodel_test.cpp
+++ b/test/target_models/spritemodel_test.cpp
@@ -1,5 +1,6 @@
 #include <QtTest/QSignalSpy>
 #include <scratchcpp/costume.h>
+#include <scratchcpp/value.h>
 #include <spritemodel.h>
 #include <graphicseffect.h>
 #include <renderedtargetmock.h>
@@ -358,6 +359,21 @@ TEST(SpriteModelTest, TouchingPoint)
     ASSERT_TRUE(model.touchingPoint(-20.08, 109.47));
 }
 
+TEST(SpriteModelTest, TouchingColor)
+{
+    SpriteModel model;
+
+    RenderedTargetMock renderedTarget;
+    model.setRenderedTarget(&renderedTarget);
+
+    Value color = 123;
+    EXPECT_CALL(renderedTarget, touchingColor(color)).WillOnce(Return(false));
+    ASSERT_FALSE(model.touchingColor(color));
+
+    EXPECT_CALL(renderedTarget, touchingColor(color)).WillOnce(Return(true));
+    ASSERT_TRUE(model.touchingColor(color));
+}
+
 TEST(SpriteModelTest, RenderedTarget)
 {
     SpriteModel model;
diff --git a/test/target_models/stagemodel_test.cpp b/test/target_models/stagemodel_test.cpp
index 2c4bf69..62a1974 100644
--- a/test/target_models/stagemodel_test.cpp
+++ b/test/target_models/stagemodel_test.cpp
@@ -1,5 +1,6 @@
 #include <QtTest/QSignalSpy>
 #include <scratchcpp/costume.h>
+#include <scratchcpp/value.h>
 #include <stagemodel.h>
 #include <graphicseffect.h>
 #include <renderedtargetmock.h>
@@ -151,6 +152,21 @@ TEST(StageModelTest, TouchingPoint)
     ASSERT_TRUE(model.touchingPoint(-20.08, 109.47));
 }
 
+TEST(StageModelTest, TouchingColor)
+{
+    StageModel model;
+
+    RenderedTargetMock renderedTarget;
+    model.setRenderedTarget(&renderedTarget);
+
+    Value color = 123;
+    EXPECT_CALL(renderedTarget, touchingColor(color)).WillOnce(Return(false));
+    ASSERT_FALSE(model.touchingColor(color));
+
+    EXPECT_CALL(renderedTarget, touchingColor(color)).WillOnce(Return(true));
+    ASSERT_TRUE(model.touchingColor(color));
+}
+
 TEST(StageModelTest, RenderedTarget)
 {
     StageModel model;

From 47de55120847a376eda690bcef14d711c637bf2f Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Mon, 8 Apr 2024 19:20:48 +0200
Subject: [PATCH 60/70] RenderedTarget: Fix index out of range in
 colorAtScratchPoint()

---
 src/renderedtarget.cpp                      |  4 ++--
 test/renderedtarget/renderedtarget_test.cpp | 12 ++++++------
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index c22ff3d..c2c10ab 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -591,8 +591,8 @@ QRgb RenderedTarget::colorAtScratchPoint(double x, double y) const
 
     // Translate the coordinates
     QPointF point = mapFromScratchToLocal(QPointF(x, y));
-    x = point.x();
-    y = point.y();
+    x = std::floor(point.x());
+    y = std::floor(point.y());
 
     const double width = m_cpuTexture.width();
     const double height = m_cpuTexture.height();
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index c9bbdb3..f24ec28 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -402,19 +402,19 @@ TEST_F(RenderedTargetTest, CpuRendering)
 
     ASSERT_EQ(target.colorAtScratchPoint(-227.5, 164), 0);             // [0, 1]
     ASSERT_EQ(target.colorAtScratchPoint(-226.5, 164), 4278190335);    // [1, 1]
-    ASSERT_EQ(target.colorAtScratchPoint(-226.1, 163.75), 4278255615); // [1.4, 1.25]
+    ASSERT_EQ(target.colorAtScratchPoint(-226.1, 163.75), 4278190335); // [1.4, 1.25]
     ASSERT_EQ(target.colorAtScratchPoint(-225.5, 164), 4294902015);    // [2, 1]
     ASSERT_EQ(target.colorAtScratchPoint(-224.5, 164), 4294934656);    // [3, 1]
 
     ASSERT_EQ(target.colorAtScratchPoint(-226.5, 163), 4278190208); // [1, 2]
     ASSERT_EQ(target.colorAtScratchPoint(-225.5, 163), 0);          // [2, 2]
     ASSERT_EQ(target.colorAtScratchPoint(-224.5, 163), 2505545047); // [3, 2]
-    ASSERT_EQ(target.colorAtScratchPoint(-224, 162.9), 9764864);    // [3.5, 2.1]
+    ASSERT_EQ(target.colorAtScratchPoint(-224, 162.9), 2505545047); // [3.5, 2.1]
 
-    ASSERT_EQ(target.colorAtScratchPoint(-226.5, 162), 4286578816); // [1, 3]
-    ASSERT_EQ(target.colorAtScratchPoint(-225.5, 162), 4286611711); // [2, 3]
-    ASSERT_EQ(target.colorAtScratchPoint(-224.5, 162), 4286611456); // [3, 3]
-    ASSERT_EQ(target.colorAtScratchPoint(-224.2, 161.5), 0);        // [3.3, 3.5]
+    ASSERT_EQ(target.colorAtScratchPoint(-226.5, 162), 4286578816);   // [1, 3]
+    ASSERT_EQ(target.colorAtScratchPoint(-225.5, 162), 4286611711);   // [2, 3]
+    ASSERT_EQ(target.colorAtScratchPoint(-224.5, 162), 4286611456);   // [3, 3]
+    ASSERT_EQ(target.colorAtScratchPoint(-224.2, 161.5), 4286611456); // [3.3, 3.5]
 
     // Cleanup
     context.doneCurrent();

From 13dd9a6ad0ac8b6585b5ed500455cbde0599e747 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 9 Apr 2024 18:19:17 +0200
Subject: [PATCH 61/70] Add removeTexture() method to CpuTextureManager

---
 src/cputexturemanager.cpp               | 15 ++++++++++++++
 src/cputexturemanager.h                 |  2 ++
 test/texture/cputexturemanager_test.cpp | 26 ++++++++++++++++++++++++-
 3 files changed, 42 insertions(+), 1 deletion(-)

diff --git a/src/cputexturemanager.cpp b/src/cputexturemanager.cpp
index d3d3b26..00b2671 100644
--- a/src/cputexturemanager.cpp
+++ b/src/cputexturemanager.cpp
@@ -51,6 +51,21 @@ const std::vector<QPoint> &CpuTextureManager::getTextureConvexHullPoints(const T
         return it->second;
 }
 
+void CpuTextureManager::removeTexture(const Texture &texture)
+{
+    if (!texture.isValid())
+        return;
+
+    const GLuint handle = texture.handle();
+    auto it = m_textureData.find(handle);
+
+    if (it != m_textureData.cend()) {
+        delete it->second;
+        m_textureData.erase(it);
+        m_convexHullPoints.erase(handle);
+    }
+}
+
 bool CpuTextureManager::addTexture(const Texture &texture)
 {
     if (!texture.isValid())
diff --git a/src/cputexturemanager.h b/src/cputexturemanager.h
index 6b5cace..561e9d5 100644
--- a/src/cputexturemanager.h
+++ b/src/cputexturemanager.h
@@ -20,6 +20,8 @@ class CpuTextureManager
         GLubyte *getTextureData(const Texture &texture);
         const std::vector<QPoint> &getTextureConvexHullPoints(const Texture &texture);
 
+        void removeTexture(const Texture &texture);
+
     private:
         bool addTexture(const Texture &texture);
 
diff --git a/test/texture/cputexturemanager_test.cpp b/test/texture/cputexturemanager_test.cpp
index 375f59c..ca6ca7b 100644
--- a/test/texture/cputexturemanager_test.cpp
+++ b/test/texture/cputexturemanager_test.cpp
@@ -36,15 +36,24 @@ class ImagePainter
             QOpenGLFramebufferObjectFormat format;
             format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
 
-            // Begin painting
             m_fbo = std::make_unique<QOpenGLFramebufferObject>(4, 6, format);
+            paint(painter, fileName);
+        }
+
+        void paint(QNanoPainter *painter, const QString &fileName)
+        {
+            // Begin painting
             m_fbo->bind();
             painter->beginFrame(m_fbo->width(), m_fbo->height());
+            glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+            glClear(GL_COLOR_BUFFER_BIT);
 
             // Paint
             QNanoImage image = QNanoImage::fromCache(painter, fileName);
             painter->drawImage(image, 0, 0);
             painter->endFrame();
+
+            m_fbo->release();
         }
 
         ~ImagePainter() { m_fbo->release(); }
@@ -102,6 +111,21 @@ TEST_F(CpuTextureManagerTest, TextureDataAndHullPoints)
         ASSERT_EQ(hullPoints2, refHullPoints2);
     }
 
+    // Test removeTexture()
+    imgPainter1.paint(&painter, "image.jpg");
+    Texture texture(imgPainter1.fbo()->texture(), imgPainter1.fbo()->size());
+    GLubyte *data = manager.getTextureData(texture);
+    ASSERT_EQ(memcmp(data, refData1, 96), 0);
+    const auto &hullPoints1 = manager.getTextureConvexHullPoints(texture);
+    ASSERT_EQ(hullPoints1, refHullPoints1);
+
+    imgPainter1.fbo()->toImage().save("/home/adazem009/test.png");
+    manager.removeTexture(texture);
+    data = manager.getTextureData(texture);
+    ASSERT_EQ(memcmp(data, refData2, 96), 0);
+    const auto &hullPoints2 = manager.getTextureConvexHullPoints(texture);
+    ASSERT_EQ(hullPoints2, refHullPoints2);
+
     // Cleanup
     context.doneCurrent();
 }

From 270ebd9cc180c7e04e8388324977ef06ddcf5ddb Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 9 Apr 2024 19:40:56 +0200
Subject: [PATCH 62/70] Add texture data methods to PenLayer

---
 src/ipenlayer.h                 |   4 ++
 src/penlayer.cpp                | 100 ++++++++++++++++++++++++++++++++
 src/penlayer.h                  |  16 ++++-
 test/mocks/penlayermock.h       |   3 +
 test/penlayer/penlayer_test.cpp |  73 +++++++++++++++++++++++
 5 files changed, 195 insertions(+), 1 deletion(-)

diff --git a/src/ipenlayer.h b/src/ipenlayer.h
index 54377e6..d21d26e 100644
--- a/src/ipenlayer.h
+++ b/src/ipenlayer.h
@@ -3,6 +3,7 @@
 #pragma once
 
 #include <qnanoquickitem.h>
+#include <scratchcpp/rect.h>
 
 namespace libscratchcpp
 {
@@ -37,6 +38,9 @@ class IPenLayer : public QNanoQuickItem
         virtual void drawLine(const PenAttributes &penAttributes, double x0, double y0, double x1, double y1) = 0;
 
         virtual QOpenGLFramebufferObject *framebufferObject() const = 0;
+        virtual QRgb colorAtScratchPoint(double x, double y) const = 0;
+
+        virtual const libscratchcpp::Rect &getBounds() const = 0;
 };
 
 } // namespace scratchcpprender
diff --git a/src/penlayer.cpp b/src/penlayer.cpp
index 66a0c2e..af75526 100644
--- a/src/penlayer.cpp
+++ b/src/penlayer.cpp
@@ -76,6 +76,8 @@ void scratchcpprender::PenLayer::clear()
     m_glF->glEnable(GL_SCISSOR_TEST);
     m_fbo->release();
 
+    m_textureDirty = true;
+    m_boundsDirty = true;
     update();
 }
 
@@ -121,6 +123,8 @@ void scratchcpprender::PenLayer::drawLine(const PenAttributes &penAttributes, do
     painter.end();
     m_fbo->release();
 
+    m_textureDirty = true;
+    m_boundsDirty = true;
     update();
 }
 
@@ -129,6 +133,84 @@ QOpenGLFramebufferObject *PenLayer::framebufferObject() const
     return m_fbo.get();
 }
 
+QRgb PenLayer::colorAtScratchPoint(double x, double y) const
+{
+    if (m_textureDirty)
+        const_cast<PenLayer *>(this)->updateTexture();
+
+    if (!m_texture.isValid())
+        return qRgba(0, 0, 0, 0);
+
+    const double width = m_texture.width();
+    const double height = m_texture.height();
+
+    // Translate the coordinates
+    // TODO: Apply scale
+    x = std::floor(x + width / 2.0);
+    y = std::floor(-y + height / 2.0);
+
+    // If the point is outside the texture, return fully transparent color
+    if ((x < 0 || x >= width) || (y < 0 || y >= height))
+        return qRgba(0, 0, 0, 0);
+
+    GLubyte *data = m_textureManager.getTextureData(m_texture);
+    const int index = (y * width + x) * 4; // RGBA channels
+    Q_ASSERT(index >= 0 && index < width * height * 4);
+    return qRgba(data[index], data[index + 1], data[index + 2], data[index + 3]);
+}
+
+const libscratchcpp::Rect &PenLayer::getBounds() const
+{
+    if (m_textureDirty)
+        const_cast<PenLayer *>(this)->updateTexture();
+
+    if (m_boundsDirty) {
+        if (!m_texture.isValid()) {
+            m_bounds = libscratchcpp::Rect();
+            return m_bounds;
+        }
+
+        m_boundsDirty = false;
+        double left = std::numeric_limits<double>::infinity();
+        double top = -std::numeric_limits<double>::infinity();
+        double right = -std::numeric_limits<double>::infinity();
+        double bottom = std::numeric_limits<double>::infinity();
+        const double width = m_texture.width();
+        const double height = m_texture.height();
+        const std::vector<QPoint> &points = m_textureManager.getTextureConvexHullPoints(m_texture);
+
+        if (points.empty()) {
+            m_bounds = libscratchcpp::Rect();
+            return m_bounds;
+        }
+
+        for (const QPointF &point : points) {
+            // TODO: Apply scale
+            double x = point.x() - width / 2;
+            double y = -point.y() + height / 2;
+
+            if (x < left)
+                left = x;
+
+            if (x > right)
+                right = x;
+
+            if (y > top)
+                top = y;
+
+            if (y < bottom)
+                bottom = y;
+        }
+
+        m_bounds.setLeft(left);
+        m_bounds.setTop(top);
+        m_bounds.setRight(right + 1);
+        m_bounds.setBottom(bottom - 1);
+    }
+
+    return m_bounds;
+}
+
 IPenLayer *PenLayer::getProjectPenLayer(libscratchcpp::IEngine *engine)
 {
     auto it = m_projectPenLayers.find(engine);
@@ -148,3 +230,21 @@ QNanoQuickItemPainter *PenLayer::createItemPainter() const
 {
     return new PenLayerPainter;
 }
+
+void PenLayer::updateTexture()
+{
+    if (!m_fbo)
+        return;
+
+    m_textureDirty = false;
+    m_textureManager.removeTexture(m_texture);
+
+    if (!m_resolvedFbo || m_resolvedFbo->size() != m_fbo->size()) {
+        QOpenGLFramebufferObjectFormat format;
+        format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
+        m_resolvedFbo = std::make_unique<QOpenGLFramebufferObject>(m_fbo->size(), format);
+    }
+
+    QOpenGLFramebufferObject::blitFramebuffer(m_resolvedFbo.get(), m_fbo.get());
+    m_texture = Texture(m_resolvedFbo->texture(), m_resolvedFbo->size());
+}
diff --git a/src/penlayer.h b/src/penlayer.h
index f1243d9..55d7e27 100644
--- a/src/penlayer.h
+++ b/src/penlayer.h
@@ -2,13 +2,16 @@
 
 #pragma once
 
-#include <ipenlayer.h>
 #include <QOpenGLFramebufferObject>
 #include <QOpenGLPaintDevice>
 #include <QOpenGLFunctions>
 #include <QPainter>
 #include <scratchcpp/iengine.h>
 
+#include "ipenlayer.h"
+#include "texture.h"
+#include "cputexturemanager.h"
+
 namespace scratchcpprender
 {
 
@@ -33,6 +36,9 @@ class PenLayer : public IPenLayer
         void drawLine(const PenAttributes &penAttributes, double x0, double y0, double x1, double y1) override;
 
         QOpenGLFramebufferObject *framebufferObject() const override;
+        QRgb colorAtScratchPoint(double x, double y) const override;
+
+        const libscratchcpp::Rect &getBounds() const override;
 
         static IPenLayer *getProjectPenLayer(libscratchcpp::IEngine *engine);
         static void addPenLayer(libscratchcpp::IEngine *engine, IPenLayer *penLayer); // for tests
@@ -44,13 +50,21 @@ class PenLayer : public IPenLayer
         QNanoQuickItemPainter *createItemPainter() const override;
 
     private:
+        void updateTexture();
+
         static std::unordered_map<libscratchcpp::IEngine *, IPenLayer *> m_projectPenLayers;
         bool m_antialiasingEnabled = true;
         libscratchcpp::IEngine *m_engine = nullptr;
         std::unique_ptr<QOpenGLFramebufferObject> m_fbo;
+        std::unique_ptr<QOpenGLFramebufferObject> m_resolvedFbo;
         std::unique_ptr<QOpenGLPaintDevice> m_paintDevice;
         QOpenGLFramebufferObjectFormat m_fboFormat;
         std::unique_ptr<QOpenGLFunctions> m_glF;
+        Texture m_texture;
+        bool m_textureDirty = true;
+        mutable CpuTextureManager m_textureManager;
+        mutable bool m_boundsDirty = true;
+        mutable libscratchcpp::Rect m_bounds;
 };
 
 } // namespace scratchcpprender
diff --git a/test/mocks/penlayermock.h b/test/mocks/penlayermock.h
index 906d634..2f8a0d5 100644
--- a/test/mocks/penlayermock.h
+++ b/test/mocks/penlayermock.h
@@ -23,6 +23,9 @@ class PenLayerMock : public IPenLayer
         MOCK_METHOD(void, drawLine, (const PenAttributes &, double, double, double, double), (override));
 
         MOCK_METHOD(QOpenGLFramebufferObject *, framebufferObject, (), (const, override));
+        MOCK_METHOD(QRgb, colorAtScratchPoint, (double, double), (const, override));
+
+        MOCK_METHOD(const libscratchcpp::Rect &, getBounds, (), (const, override));
 
         MOCK_METHOD(QNanoQuickItemPainter *, createItemPainter, (), (const, override));
 };
diff --git a/test/penlayer/penlayer_test.cpp b/test/penlayer/penlayer_test.cpp
index 539a7e8..01ce44b 100644
--- a/test/penlayer/penlayer_test.cpp
+++ b/test/penlayer/penlayer_test.cpp
@@ -257,3 +257,76 @@ TEST_F(PenLayerTest, DrawLine)
     buffer.open(QFile::ReadOnly);
     ASSERT_EQ(ref.readAll(), buffer.readAll());
 }
+
+TEST_F(PenLayerTest, TextureData)
+{
+    PenLayer penLayer;
+    penLayer.setAntialiasingEnabled(false);
+    EngineMock engine;
+    EXPECT_CALL(engine, stageWidth()).WillRepeatedly(Return(6));
+    EXPECT_CALL(engine, stageHeight()).WillRepeatedly(Return(4));
+    penLayer.setEngine(&engine);
+
+    PenAttributes attr;
+    attr.color = QColor(255, 0, 0);
+    attr.diameter = 1;
+    penLayer.drawLine(attr, -3, 2, 3, -2);
+    ASSERT_EQ(penLayer.colorAtScratchPoint(-3, 2), qRgb(255, 0, 0));
+    ASSERT_EQ(penLayer.colorAtScratchPoint(0, 2), qRgba(0, 0, 0, 0));
+    ASSERT_EQ(penLayer.colorAtScratchPoint(-1, 1), qRgb(255, 0, 0));
+
+    Rect bounds = penLayer.getBounds();
+    ASSERT_EQ(bounds.left(), -3);
+    ASSERT_EQ(bounds.top(), 2);
+    ASSERT_EQ(bounds.right(), 3);
+    ASSERT_EQ(bounds.bottom(), -2);
+
+    attr.color = QColor(0, 128, 0, 128);
+    attr.diameter = 2;
+    penLayer.drawLine(attr, -3, -2, 3, 2);
+    ASSERT_EQ(penLayer.colorAtScratchPoint(-3, 2), qRgb(255, 0, 0));
+    ASSERT_EQ(penLayer.colorAtScratchPoint(0, 2), qRgba(0, 64, 0, 128));
+    ASSERT_EQ(penLayer.colorAtScratchPoint(-1, 1), qRgb(127, 64, 0));
+
+    bounds = penLayer.getBounds();
+    ASSERT_EQ(bounds.left(), -3);
+    ASSERT_EQ(bounds.top(), 2);
+    ASSERT_EQ(bounds.right(), 3);
+    ASSERT_EQ(bounds.bottom(), -2);
+
+    penLayer.clear();
+    ASSERT_EQ(penLayer.colorAtScratchPoint(-3, 2), 0);
+    ASSERT_EQ(penLayer.colorAtScratchPoint(0, 2), 0);
+    ASSERT_EQ(penLayer.colorAtScratchPoint(-1, 1), 0);
+
+    bounds = penLayer.getBounds();
+    ASSERT_EQ(bounds.left(), 0);
+    ASSERT_EQ(bounds.top(), 0);
+    ASSERT_EQ(bounds.right(), 0);
+    ASSERT_EQ(bounds.bottom(), 0);
+
+    attr.color = QColor(0, 255, 0, 255);
+    attr.diameter = 1;
+    penLayer.drawLine(attr, 0, -1, 1, 1);
+    ASSERT_EQ(penLayer.colorAtScratchPoint(0, 1), qRgb(0, 255, 0));
+    ASSERT_EQ(penLayer.colorAtScratchPoint(0, 0), qRgb(0, 255, 0));
+    ASSERT_EQ(penLayer.colorAtScratchPoint(-3, 1), qRgba(0, 0, 0, 0));
+
+    bounds = penLayer.getBounds();
+    ASSERT_EQ(bounds.left(), 0);
+    ASSERT_EQ(bounds.top(), 1);
+    ASSERT_EQ(bounds.right(), 1);
+    ASSERT_EQ(bounds.bottom(), -1);
+
+    attr.diameter = 2;
+    penLayer.drawPoint(attr, -2, 0);
+    ASSERT_EQ(penLayer.colorAtScratchPoint(0, 1), qRgb(0, 255, 0));
+    ASSERT_EQ(penLayer.colorAtScratchPoint(0, 0), qRgb(0, 255, 0));
+    ASSERT_EQ(penLayer.colorAtScratchPoint(-3, 1), qRgb(0, 255, 0));
+
+    bounds = penLayer.getBounds();
+    ASSERT_EQ(bounds.left(), -3);
+    ASSERT_EQ(bounds.top(), 1);
+    ASSERT_EQ(bounds.right(), 1);
+    ASSERT_EQ(bounds.bottom(), -1);
+}

From 401ed3d4cfcc2de93a86ca99f906050320704c1e Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 9 Apr 2024 19:57:45 +0200
Subject: [PATCH 63/70] Check the pen layer in touchingColor()

---
 src/renderedtarget.cpp                      | 40 +++++++++++++++++----
 src/renderedtarget.h                        |  5 ++-
 test/renderedtarget/renderedtarget_test.cpp | 16 +++++++++
 3 files changed, 53 insertions(+), 8 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index c2c10ab..223db46 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -15,6 +15,7 @@
 #include "bitmapskin.h"
 #include "svgskin.h"
 #include "cputexturemanager.h"
+#include "penlayer.h"
 
 using namespace scratchcpprender;
 using namespace libscratchcpp;
@@ -216,6 +217,7 @@ void RenderedTarget::setEngine(IEngine *newEngine)
     m_texture = Texture();
     m_oldTexture = Texture();
     m_cpuTexture = Texture();
+    m_penLayer = PenLayer::getProjectPenLayer(m_engine);
     m_convexHullDirty = true;
     clearGraphicEffects();
     m_hullPoints.clear();
@@ -941,6 +943,10 @@ QRectF RenderedTarget::candidatesBounds(const QRectF &targetRect, const std::vec
         }
     }
 
+    // Check pen layer
+    if (m_penLayer)
+        united = united.united(rectIntersection(targetRect, m_penLayer->getBounds()));
+
     return united;
 }
 
@@ -980,14 +986,19 @@ QRectF RenderedTarget::candidateIntersection(const QRectF &targetRect, IRendered
     if (target) {
         // Calculate the intersection of the bounding rectangles
         Rect scratchRect = target->getFastBounds();
-        // TODO: Use Rect::snapToInt()
-        QRect rect(QPoint(scratchRect.left(), scratchRect.bottom()), QPoint(scratchRect.right(), scratchRect.top()));
-        return targetRect.intersected(rect);
+        return rectIntersection(targetRect, scratchRect);
     }
 
     return QRectF();
 }
 
+QRectF RenderedTarget::rectIntersection(const QRectF &targetRect, const Rect &candidateRect)
+{
+    // TODO: Use Rect::snapToInt()
+    QRect rect(QPoint(candidateRect.left(), candidateRect.bottom()), QPoint(candidateRect.right(), candidateRect.top()));
+    return targetRect.intersected(rect);
+}
+
 void RenderedTarget::clampRect(Rect &rect, double left, double right, double bottom, double top)
 {
     // TODO: Use Rect::clamp()
@@ -1035,16 +1046,31 @@ bool RenderedTarget::colorMatches(QRgb a, QRgb b)
     return (qRed(a) & 0b11111000) == (qRed(b) & 0b11111000) && (qGreen(a) & 0b11111000) == (qGreen(b) & 0b11111000) && (qBlue(a) & 0b11110000) == (qBlue(b) & 0b11110000);
 }
 
-QRgb RenderedTarget::sampleColor3b(const QPointF &point, const std::vector<IRenderedTarget *> &targets)
+QRgb RenderedTarget::sampleColor3b(const QPointF &point, const std::vector<IRenderedTarget *> &targets) const
 {
     // https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L1966-L1990
     double blendAlpha = 1;
     QRgb blendColor;
     int r = 0, g = 0, b = 0;
+    bool penLayerChecked = false;
 
-    for (int i = 0; blendAlpha != 0 && i < targets.size(); i++) {
-        Q_ASSERT(targets[i]);
-        blendColor = targets[i]->colorAtScratchPoint(point.x(), point.y());
+    for (int i = 0; blendAlpha != 0 && i <= targets.size(); i++) { // NOTE: <= instead of < to process the pen layer
+        Q_ASSERT(i == targets.size() || targets[i]);
+
+        if ((i == targets.size() || targets[i]->stageModel()) && !penLayerChecked) {
+            if (m_penLayer)
+                blendColor = m_penLayer->colorAtScratchPoint(point.x(), point.y());
+            else
+                blendColor = qRgba(0, 0, 0, 0);
+
+            penLayerChecked = true;
+
+            if (i < targets.size())
+                i--; // check stage on next iteration
+        } else if (i == targets.size())
+            break;
+        else
+            blendColor = targets[i]->colorAtScratchPoint(point.x(), point.y());
 
         r += qRed(blendColor) * blendAlpha;
         g += qGreen(blendColor) * blendAlpha;
diff --git a/src/renderedtarget.h b/src/renderedtarget.h
index 8c1a28e..8a37e95 100644
--- a/src/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -20,6 +20,7 @@ namespace scratchcpprender
 
 class Skin;
 class CpuTextureManager;
+class IPenLayer;
 
 class RenderedTarget : public IRenderedTarget
 {
@@ -131,16 +132,18 @@ class RenderedTarget : public IRenderedTarget
         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;
         static QRectF candidateIntersection(const QRectF &targetRect, IRenderedTarget *target);
+        static QRectF rectIntersection(const QRectF &targetRect, const libscratchcpp::Rect &candidateRect);
         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 QRgb sampleColor3b(const QPointF &point, const std::vector<IRenderedTarget *> &targets);
+        QRgb sampleColor3b(const QPointF &point, const std::vector<IRenderedTarget *> &targets) const;
 
         libscratchcpp::IEngine *m_engine = nullptr;
         libscratchcpp::Costume *m_costume = nullptr;
         StageModel *m_stageModel = nullptr;
         SpriteModel *m_spriteModel = nullptr;
         SceneMouseArea *m_mouseArea = nullptr;
+        IPenLayer *m_penLayer = nullptr;
         bool m_costumesLoaded = false;
         std::unordered_map<libscratchcpp::Costume *, Skin *> m_skins;
         bool m_skinsInherited = false;
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index f24ec28..6107112 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -6,6 +6,7 @@
 #include <stagemodel.h>
 #include <spritemodel.h>
 #include <scenemousearea.h>
+#include <penlayer.h>
 #include <scratchcpp/stage.h>
 #include <scratchcpp/sprite.h>
 #include <scratchcpp/costume.h>
@@ -13,6 +14,7 @@
 #include <scratchcpp/value.h>
 #include <enginemock.h>
 #include <renderedtargetmock.h>
+#include <penlayermock.h>
 
 #include "../common.h"
 
@@ -1023,6 +1025,9 @@ TEST_F(RenderedTargetTest, TouchingColor)
     parent.setWidth(480);
     parent.setHeight(360);
 
+    PenLayerMock penLayer;
+    PenLayer::addPenLayer(&engine, &penLayer);
+
     RenderedTarget target(&parent);
     target.setEngine(&engine);
     target.setSpriteModel(&model);
@@ -1049,7 +1054,12 @@ TEST_F(RenderedTargetTest, TouchingColor)
     target.updateCostume(costume.get());
     target.beforeRedraw();
 
+    Rect penBounds(5, 1, 6, -5);
     EXPECT_CALL(engine, targets()).WillRepeatedly(ReturnRef(targets));
+    EXPECT_CALL(stageTarget, stageModel()).WillRepeatedly(Return(&stageModel));
+    EXPECT_CALL(target1, stageModel()).WillRepeatedly(Return(nullptr));
+    EXPECT_CALL(target2, stageModel()).WillRepeatedly(Return(nullptr));
+    EXPECT_CALL(penLayer, getBounds()).WillRepeatedly(ReturnRef(penBounds));
 
     static const Value color1 = 4286611711; // "purple"
     static const Value color2 = 596083443;  // close to color1 and transparent
@@ -1071,6 +1081,7 @@ TEST_F(RenderedTargetTest, TouchingColor)
     EXPECT_CALL(target1, colorAtScratchPoint(2, -1)).WillOnce(Return(color3Int));
     EXPECT_CALL(target2, colorAtScratchPoint(3, -1)).WillOnce(Return(color4.toInt()));
     EXPECT_CALL(target1, colorAtScratchPoint(3, -1)).WillOnce(Return(color4.toInt()));
+    EXPECT_CALL(penLayer, colorAtScratchPoint(3, -1)).WillOnce(Return(color4.toInt()));
     EXPECT_CALL(stageTarget, colorAtScratchPoint(3, -1)).WillOnce(Return(color4.toInt()));
     ASSERT_FALSE(target.touchingColor(color1));
 
@@ -1134,6 +1145,7 @@ TEST_F(RenderedTargetTest, TouchingColor)
     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));
 
@@ -1145,6 +1157,7 @@ TEST_F(RenderedTargetTest, TouchingColor)
     EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5 - 300, -6.5 + 200, 1.8 - 300, -8 + 200)));
     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(color1));
 
@@ -1156,6 +1169,7 @@ TEST_F(RenderedTargetTest, TouchingColor)
     EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5 + 300, -6.5 + 200, 1.8 + 300, -8 + 200)));
     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(color1));
 
@@ -1167,6 +1181,7 @@ TEST_F(RenderedTargetTest, TouchingColor)
     EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5 + 300, -6.5 - 200, 1.8 + 300, -8 - 200)));
     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(color1));
 
@@ -1178,6 +1193,7 @@ TEST_F(RenderedTargetTest, TouchingColor)
     EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5 - 300, -6.5 - 200, 1.8 - 300, -8 - 200)));
     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(color1));
 

From 6f22312f2d21d0665c4ae4a6b01cb88d5911f47d Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 9 Apr 2024 20:00:03 +0200
Subject: [PATCH 64/70] RenderedTarget: Optimize point parameters

---
 src/renderedtarget.cpp | 12 ++++--------
 src/renderedtarget.h   |  2 +-
 2 files changed, 5 insertions(+), 9 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 223db46..eb62eb0 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -665,15 +665,11 @@ bool RenderedTarget::touchingColor(const Value &color) const
         return false;
     }
 
-    QPointF point;
-
     // 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)) {
-                point.setX(x);
-                point.setY(y);
-                QRgb pixelColor = sampleColor3b(point, candidates);
+                QRgb pixelColor = sampleColor3b(x, y, candidates);
 
                 if (colorMatches(rgb, pixelColor))
                     return true;
@@ -1046,7 +1042,7 @@ bool RenderedTarget::colorMatches(QRgb a, QRgb b)
     return (qRed(a) & 0b11111000) == (qRed(b) & 0b11111000) && (qGreen(a) & 0b11111000) == (qGreen(b) & 0b11111000) && (qBlue(a) & 0b11110000) == (qBlue(b) & 0b11110000);
 }
 
-QRgb RenderedTarget::sampleColor3b(const QPointF &point, const std::vector<IRenderedTarget *> &targets) const
+QRgb RenderedTarget::sampleColor3b(double x, double y, const std::vector<IRenderedTarget *> &targets) const
 {
     // https://github.com/scratchfoundation/scratch-render/blob/0a04c2fb165f5c20406ec34ab2ea5682ae45d6e0/src/RenderWebGL.js#L1966-L1990
     double blendAlpha = 1;
@@ -1059,7 +1055,7 @@ QRgb RenderedTarget::sampleColor3b(const QPointF &point, const std::vector<IRend
 
         if ((i == targets.size() || targets[i]->stageModel()) && !penLayerChecked) {
             if (m_penLayer)
-                blendColor = m_penLayer->colorAtScratchPoint(point.x(), point.y());
+                blendColor = m_penLayer->colorAtScratchPoint(x, y);
             else
                 blendColor = qRgba(0, 0, 0, 0);
 
@@ -1070,7 +1066,7 @@ QRgb RenderedTarget::sampleColor3b(const QPointF &point, const std::vector<IRend
         } else if (i == targets.size())
             break;
         else
-            blendColor = targets[i]->colorAtScratchPoint(point.x(), point.y());
+            blendColor = targets[i]->colorAtScratchPoint(x, y);
 
         r += qRed(blendColor) * blendAlpha;
         g += qGreen(blendColor) * blendAlpha;
diff --git a/src/renderedtarget.h b/src/renderedtarget.h
index 8a37e95..7c5ed0e 100644
--- a/src/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -136,7 +136,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);
-        QRgb sampleColor3b(const QPointF &point, const std::vector<IRenderedTarget *> &targets) const;
+        QRgb sampleColor3b(double x, double y, const std::vector<IRenderedTarget *> &targets) const;
 
         libscratchcpp::IEngine *m_engine = nullptr;
         libscratchcpp::Costume *m_costume = nullptr;

From 9129e7e137b610a130b0444f539d19a6dc114610 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Tue, 9 Apr 2024 20:29:38 +0200
Subject: [PATCH 65/70] README: Mark touching color block as finished

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index d1b9218..7855bf7 100644
--- a/README.md
+++ b/README.md
@@ -145,7 +145,7 @@ int main(int argc, char **argv) {
 - [x] Clones
 - [x] Sprite dragging
 - [x] Touching sprite block
-- [ ] Touching color blocks
+- [ ] Touching color blocks (color is touching color is not implemented yet)
 - [ ] Pen blocks (all blocks except the stamp block are implemented)
 - [x] Monitors
 - [ ] Graphics effects (color, brightness and ghost are implemented)

From 1f466ded8143c33f05b616a56451bd64434147c7 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 11 Apr 2024 08:59:59 +0200
Subject: [PATCH 66/70] Round sprite position

---
 src/renderedtarget.cpp                      |   4 +-
 test/renderedtarget/renderedtarget_test.cpp | 114 ++++++++++----------
 2 files changed, 59 insertions(+), 59 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index eb62eb0..9bad239 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -52,7 +52,7 @@ void RenderedTarget::updateX(double x)
     if (x == m_x)
         return;
 
-    m_x = x;
+    m_x = std::round(x);
     calculatePos();
 }
 
@@ -61,7 +61,7 @@ void RenderedTarget::updateY(double y)
     if (y == m_y)
         return;
 
-    m_y = y;
+    m_y = std::round(y);
     calculatePos();
 }
 
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index 6107112..dd9feb9 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -154,15 +154,15 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     target.updateX(12.5);
-    ASSERT_EQ(target.x(), 275.5);
+    ASSERT_EQ(target.x(), 276);
     ASSERT_EQ(target.y(), 108);
 
     // Y
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     target.updateY(-76.09);
-    ASSERT_EQ(target.x(), 275.5);
-    ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09);
+    ASSERT_EQ(target.x(), 276);
+    ASSERT_EQ(target.y(), 184);
 
     // Size
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
@@ -171,8 +171,8 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     target.beforeRedraw();
     ASSERT_EQ(target.width(), 4);
     ASSERT_EQ(target.height(), 6);
-    ASSERT_EQ(target.x(), 275.5);
-    ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09);
+    ASSERT_EQ(target.x(), 276);
+    ASSERT_EQ(target.y(), 184);
     ASSERT_EQ(target.transformOriginPoint().x(), -23);
     ASSERT_EQ(target.transformOriginPoint().y(), 72);
 
@@ -187,8 +187,8 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     target.updateRotationStyle(Sprite::RotationStyle::LeftRight);
     ASSERT_EQ(target.mirrorHorizontally(), false);
     ASSERT_EQ(target.rotation(), 0);
-    ASSERT_EQ(target.x(), 275.5);
-    ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09);
+    ASSERT_EQ(target.x(), 276);
+    ASSERT_EQ(target.y(), 184);
     ASSERT_TRUE(mirrorHorizontallySpy.empty());
 
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
@@ -196,8 +196,8 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     target.updateDirection(-15);
     ASSERT_EQ(target.mirrorHorizontally(), true);
     ASSERT_EQ(target.rotation(), 0);
-    ASSERT_EQ(target.x(), 229.5);
-    ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09);
+    ASSERT_EQ(target.x(), 230);
+    ASSERT_EQ(target.y(), 184);
     ASSERT_EQ(mirrorHorizontallySpy.count(), 1);
 
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
@@ -205,8 +205,8 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     target.updateDirection(134.89);
     ASSERT_EQ(target.mirrorHorizontally(), false);
     ASSERT_EQ(target.rotation(), 0);
-    ASSERT_EQ(target.x(), 275.5);
-    ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09);
+    ASSERT_EQ(target.x(), 276);
+    ASSERT_EQ(target.y(), 184);
     ASSERT_EQ(mirrorHorizontallySpy.count(), 2);
 
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
@@ -223,8 +223,8 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     target.beforeRedraw();
     ASSERT_EQ(target.width(), 4);
     ASSERT_EQ(target.height(), 6);
-    ASSERT_EQ(target.x(), 401.75);
-    ASSERT_EQ(std::round(target.y() * 100) / 100, 312.14);
+    ASSERT_EQ(target.x(), 402.5);
+    ASSERT_EQ(target.y(), 312);
     ASSERT_EQ(target.z(), 3);
     ASSERT_EQ(target.rotation(), 0);
     ASSERT_EQ(target.transformOriginPoint().x(), -23);
@@ -241,8 +241,8 @@ TEST_F(RenderedTargetTest, UpdateMethods)
     target.beforeRedraw();
     ASSERT_EQ(target.width(), 4);
     ASSERT_EQ(target.height(), 6);
-    ASSERT_EQ(target.x(), 378.75);
-    ASSERT_EQ(std::round(target.y() * 100) / 100, 384.14);
+    ASSERT_EQ(target.x(), 379.5);
+    ASSERT_EQ(target.y(), 384);
     ASSERT_EQ(target.transformOriginPoint().x(), 0);
     ASSERT_EQ(target.transformOriginPoint().y(), 0);
     ASSERT_EQ(target.transformOrigin(), QQuickItem::TopLeft);
@@ -263,8 +263,8 @@ TEST_F(RenderedTargetTest, UpdateMethods)
 
     ASSERT_EQ(target.width(), 26);
     ASSERT_EQ(target.height(), 26);
-    ASSERT_EQ(target.x(), 328.75);
-    ASSERT_EQ(std::round(target.y() * 100) / 100, 400.14);
+    ASSERT_EQ(target.x(), 329.5);
+    ASSERT_EQ(target.y(), 400);
     ASSERT_EQ(target.z(), 3);
     ASSERT_EQ(target.rotation(), 0);
     ASSERT_EQ(target.transformOriginPoint().x(), 50);
@@ -375,48 +375,48 @@ TEST_F(RenderedTargetTest, CpuRendering)
 
     // Test containsScratchPoint()
     target.updateDirection(0);
-    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]
+    ASSERT_FALSE(target.containsScratchPoint(-228, 165)); // [0, 0]
+    ASSERT_FALSE(target.containsScratchPoint(-227, 165)); // [1, 0]
+    ASSERT_FALSE(target.containsScratchPoint(-226, 165)); // [2, 0]
+    ASSERT_FALSE(target.containsScratchPoint(-225, 165)); // [3, 0]
+
+    ASSERT_FALSE(target.containsScratchPoint(-228, 164));     // [0, 1]
+    ASSERT_TRUE(target.containsScratchPoint(-227, 164));      // [1, 1]
+    ASSERT_TRUE(target.containsScratchPoint(-226.6, 163.75)); // [1.4, 1.25]
+    ASSERT_TRUE(target.containsScratchPoint(-226, 164));      // [2, 1]
+    ASSERT_TRUE(target.containsScratchPoint(-225, 164));      // [3, 1]
+
+    ASSERT_TRUE(target.containsScratchPoint(-227, 163));      // [1, 2]
+    ASSERT_FALSE(target.containsScratchPoint(-226, 163));     // [2, 2]
+    ASSERT_TRUE(target.containsScratchPoint(-225, 163));      // [3, 2]
+    ASSERT_FALSE(target.containsScratchPoint(-224.5, 162.9)); // [3.5, 2.1]
+
+    ASSERT_TRUE(target.containsScratchPoint(-227, 162));      // [1, 3]
+    ASSERT_TRUE(target.containsScratchPoint(-226, 162));      // [2, 3]
+    ASSERT_TRUE(target.containsScratchPoint(-225, 162));      // [3, 3]
+    ASSERT_FALSE(target.containsScratchPoint(-224.7, 161.5)); // [3.3, 3.5]
 
     // Test colorAtScratchPoint()
-    ASSERT_EQ(target.colorAtScratchPoint(-227.5, 165), 0); // [0, 0]
-    ASSERT_EQ(target.colorAtScratchPoint(-226.5, 165), 0); // [1, 0]
-    ASSERT_EQ(target.colorAtScratchPoint(-225.5, 165), 0); // [2, 0]
-    ASSERT_EQ(target.colorAtScratchPoint(-224.5, 165), 0); // [3, 0]
-
-    ASSERT_EQ(target.colorAtScratchPoint(-227.5, 164), 0);             // [0, 1]
-    ASSERT_EQ(target.colorAtScratchPoint(-226.5, 164), 4278190335);    // [1, 1]
-    ASSERT_EQ(target.colorAtScratchPoint(-226.1, 163.75), 4278190335); // [1.4, 1.25]
-    ASSERT_EQ(target.colorAtScratchPoint(-225.5, 164), 4294902015);    // [2, 1]
-    ASSERT_EQ(target.colorAtScratchPoint(-224.5, 164), 4294934656);    // [3, 1]
-
-    ASSERT_EQ(target.colorAtScratchPoint(-226.5, 163), 4278190208); // [1, 2]
-    ASSERT_EQ(target.colorAtScratchPoint(-225.5, 163), 0);          // [2, 2]
-    ASSERT_EQ(target.colorAtScratchPoint(-224.5, 163), 2505545047); // [3, 2]
-    ASSERT_EQ(target.colorAtScratchPoint(-224, 162.9), 2505545047); // [3.5, 2.1]
-
-    ASSERT_EQ(target.colorAtScratchPoint(-226.5, 162), 4286578816);   // [1, 3]
-    ASSERT_EQ(target.colorAtScratchPoint(-225.5, 162), 4286611711);   // [2, 3]
-    ASSERT_EQ(target.colorAtScratchPoint(-224.5, 162), 4286611456);   // [3, 3]
-    ASSERT_EQ(target.colorAtScratchPoint(-224.2, 161.5), 4286611456); // [3.3, 3.5]
+    ASSERT_EQ(target.colorAtScratchPoint(-228, 165), 0); // [0, 0]
+    ASSERT_EQ(target.colorAtScratchPoint(-227, 165), 0); // [1, 0]
+    ASSERT_EQ(target.colorAtScratchPoint(-226, 165), 0); // [2, 0]
+    ASSERT_EQ(target.colorAtScratchPoint(-225, 165), 0); // [3, 0]
+
+    ASSERT_EQ(target.colorAtScratchPoint(-228, 164), 0);               // [0, 1]
+    ASSERT_EQ(target.colorAtScratchPoint(-227, 164), 4278190335);      // [1, 1]
+    ASSERT_EQ(target.colorAtScratchPoint(-226.6, 163.75), 4278190335); // [1.4, 1.25]
+    ASSERT_EQ(target.colorAtScratchPoint(-226, 164), 4294902015);      // [2, 1]
+    ASSERT_EQ(target.colorAtScratchPoint(-225, 164), 4294934656);      // [3, 1]
+
+    ASSERT_EQ(target.colorAtScratchPoint(-227, 163), 4278190208);     // [1, 2]
+    ASSERT_EQ(target.colorAtScratchPoint(-226, 163), 0);              // [2, 2]
+    ASSERT_EQ(target.colorAtScratchPoint(-225, 163), 2505545047);     // [3, 2]
+    ASSERT_EQ(target.colorAtScratchPoint(-224.5, 162.9), 2505545047); // [3.5, 2.1]
+
+    ASSERT_EQ(target.colorAtScratchPoint(-227, 162), 4286578816);     // [1, 3]
+    ASSERT_EQ(target.colorAtScratchPoint(-226, 162), 4286611711);     // [2, 3]
+    ASSERT_EQ(target.colorAtScratchPoint(-225, 162), 4286611456);     // [3, 3]
+    ASSERT_EQ(target.colorAtScratchPoint(-224.7, 161.5), 4286611456); // [3.3, 3.5]
 
     // Cleanup
     context.doneCurrent();

From a2db06b2982453cea02ef73b9377514324588a5c Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 11 Apr 2024 16:20:29 +0200
Subject: [PATCH 67/70] fix #84: Use sprite dragging API

---
 src/renderedtarget.cpp                      | 12 ++++++++----
 test/renderedtarget/renderedtarget_test.cpp | 18 ++++++++++++++++++
 2 files changed, 26 insertions(+), 4 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 9bad239..3ea3035 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -486,9 +486,11 @@ void RenderedTarget::mouseReleaseEvent(QMouseEvent *event)
     Q_ASSERT(m_mouseArea);
 
     // Stop dragging
-    if (m_mouseArea->draggedSprite() == this)
+    if (m_mouseArea->draggedSprite() == this) {
+        Sprite *sprite = m_spriteModel->sprite();
+        sprite->stopDragging();
         m_mouseArea->setDraggedSprite(nullptr);
-    else if (m_engine && m_spriteModel && m_spriteModel->sprite()->draggable()) {
+    } else if (m_engine && m_spriteModel && m_spriteModel->sprite()->draggable()) {
         // Notify libscratchcpp about the click
         m_engine->clickTarget(scratchTarget());
     }
@@ -503,6 +505,7 @@ void RenderedTarget::mouseMoveEvent(QMouseEvent *event)
     if (m_clicked && !m_mouseArea->draggedSprite() && m_spriteModel && m_spriteModel->sprite()->draggable()) {
         Q_ASSERT(m_engine);
         Sprite *sprite = m_spriteModel->sprite();
+        sprite->startDragging();
         m_dragDeltaX = m_engine->mouseX() - sprite->x();
         m_dragDeltaY = m_engine->mouseY() - sprite->y();
         m_mouseArea->setDraggedSprite(this);
@@ -758,8 +761,9 @@ void RenderedTarget::handleSceneMouseMove(qreal x, qreal y)
         Q_ASSERT(m_spriteModel && m_spriteModel->sprite());
         Q_ASSERT(m_engine);
         Sprite *sprite = m_spriteModel->sprite();
-        sprite->setX(x / m_stageScale - m_engine->stageWidth() / 2.0 - m_dragDeltaX);
-        sprite->setY(-y / m_stageScale + m_engine->stageHeight() / 2.0 - m_dragDeltaY);
+        const double dragX = x / m_stageScale - m_engine->stageWidth() / 2.0 - m_dragDeltaX;
+        const double dragY = -y / m_stageScale + m_engine->stageHeight() / 2.0 - m_dragDeltaY;
+        sprite->dragToPosition(dragX, dragY);
     }
 }
 
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index dd9feb9..696268e 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -443,6 +443,7 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     ASSERT_EQ(sprite.x(), 64.08);
     ASSERT_EQ(sprite.y(), -6.86);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    ASSERT_FALSE(sprite.dragging());
 
     // Try right mouse button (should not work)
     QMouseEvent moveEventRightButton(QEvent::MouseMove, QPointF(), QPointF(), Qt::RightButton, Qt::RightButton, Qt::NoModifier);
@@ -454,14 +455,17 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     ASSERT_EQ(sprite.x(), 64.08);
     ASSERT_EQ(sprite.y(), -6.86);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    ASSERT_FALSE(sprite.dragging());
     EXPECT_CALL(engine, clickTarget).Times(0);
     QCoreApplication::sendEvent(&target, &releaseEventRightButton);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    ASSERT_FALSE(sprite.dragging());
 
     emit mouseArea.mouseMoved(1064, 651);
     ASSERT_EQ(sprite.x(), 64.08);
     ASSERT_EQ(sprite.y(), -6.86);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    ASSERT_FALSE(sprite.dragging());
 
     // Try right mouse button with "draggable" set to true (should not work)
     sprite.setDraggable(true);
@@ -471,14 +475,17 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     ASSERT_EQ(sprite.x(), 64.08);
     ASSERT_EQ(sprite.y(), -6.86);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    ASSERT_FALSE(sprite.dragging());
     EXPECT_CALL(engine, clickTarget(&sprite));
     QCoreApplication::sendEvent(&target, &releaseEventRightButton);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    ASSERT_FALSE(sprite.dragging());
 
     emit mouseArea.mouseMoved(1064, 651);
     ASSERT_EQ(sprite.x(), 64.08);
     ASSERT_EQ(sprite.y(), -6.86);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    ASSERT_FALSE(sprite.dragging());
 
     // Try left mouse button (should not work with "draggable" set to false)
     sprite.setDraggable(false);
@@ -491,11 +498,13 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     ASSERT_EQ(sprite.x(), 64.08);
     ASSERT_EQ(sprite.y(), -6.86);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    ASSERT_FALSE(sprite.dragging());
 
     emit mouseArea.mouseMoved(1064, 651);
     ASSERT_EQ(sprite.x(), 64.08);
     ASSERT_EQ(sprite.y(), -6.86);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    ASSERT_FALSE(sprite.dragging());
     EXPECT_CALL(engine, clickTarget).Times(0);
     QCoreApplication::sendEvent(&target, &releaseEvent);
 
@@ -510,6 +519,7 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     ASSERT_EQ(sprite.x(), 64.08);
     ASSERT_EQ(sprite.y(), -6.86);
     ASSERT_EQ(mouseArea.draggedSprite(), &target);
+    ASSERT_TRUE(sprite.dragging());
 
     // Drag
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
@@ -518,6 +528,7 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     ASSERT_EQ(std::round(sprite.x() * 100) / 100, 61.22);
     ASSERT_EQ(std::round(sprite.y() * 100) / 100, -14.41);
     ASSERT_EQ(mouseArea.draggedSprite(), &target);
+    ASSERT_TRUE(sprite.dragging());
 
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
@@ -525,6 +536,7 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     ASSERT_EQ(std::round(sprite.x() * 100) / 100, 68.26);
     ASSERT_EQ(std::round(sprite.y() * 100) / 100, -1.95);
     ASSERT_EQ(mouseArea.draggedSprite(), &target);
+    ASSERT_TRUE(sprite.dragging());
 
     // Create another sprite
     RenderedTarget anotherTarget;
@@ -540,12 +552,16 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     anotherTarget.setStageScale(3.5);
     anotherTarget.setMouseArea(&mouseArea);
 
+    ASSERT_FALSE(anotherSprite.dragging());
+
     // Try to drag the second sprite while the first is being dragged
     sprite.setDraggable(true);
     EXPECT_CALL(engine, clickTarget).Times(0);
     QCoreApplication::sendEvent(&anotherTarget, &pressEvent);
     QCoreApplication::sendEvent(&anotherTarget, &moveEvent);
     ASSERT_EQ(mouseArea.draggedSprite(), &target);
+    ASSERT_TRUE(sprite.dragging());
+    ASSERT_FALSE(anotherSprite.dragging());
     EXPECT_CALL(engine, clickTarget(&sprite));
     QCoreApplication::sendEvent(&anotherTarget, &releaseEvent);
 
@@ -555,6 +571,8 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     ASSERT_EQ(std::round(sprite.x() * 100) / 100, 68.26);
     ASSERT_EQ(std::round(sprite.y() * 100) / 100, -1.95);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
+    ASSERT_FALSE(sprite.dragging());
+    ASSERT_FALSE(anotherSprite.dragging());
 }
 
 TEST_F(RenderedTargetTest, Engine)

From 85f7a56767a319ff176971a50b2813316477da16 Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Thu, 11 Apr 2024 17:54:17 +0200
Subject: [PATCH 68/70] fix #85: Update drag position in beforeRedraw()

---
 src/renderedtarget.cpp                      | 16 +++++++++++-----
 src/renderedtarget.h                        |  2 ++
 test/renderedtarget/renderedtarget_test.cpp | 10 ++++++++--
 3 files changed, 21 insertions(+), 7 deletions(-)

diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp
index 3ea3035..38c7147 100644
--- a/src/renderedtarget.cpp
+++ b/src/renderedtarget.cpp
@@ -180,6 +180,15 @@ void RenderedTarget::beforeRedraw()
         m_oldTexture = m_texture;
         update();
     }
+
+    // Update drag position
+    if (m_spriteModel) {
+        Sprite *sprite = m_spriteModel->sprite();
+        Q_ASSERT(sprite);
+
+        if (sprite->dragging())
+            sprite->dragToPosition(m_dragX, m_dragY);
+    }
 }
 
 void RenderedTarget::deinitClone()
@@ -758,12 +767,9 @@ void RenderedTarget::handleSceneMouseMove(qreal x, qreal y)
     Q_ASSERT(m_mouseArea);
 
     if (m_mouseArea->draggedSprite() == this) {
-        Q_ASSERT(m_spriteModel && m_spriteModel->sprite());
         Q_ASSERT(m_engine);
-        Sprite *sprite = m_spriteModel->sprite();
-        const double dragX = x / m_stageScale - m_engine->stageWidth() / 2.0 - m_dragDeltaX;
-        const double dragY = -y / m_stageScale + m_engine->stageHeight() / 2.0 - m_dragDeltaY;
-        sprite->dragToPosition(dragX, dragY);
+        m_dragX = x / m_stageScale - m_engine->stageWidth() / 2.0 - m_dragDeltaX;
+        m_dragY = -y / m_stageScale + m_engine->stageHeight() / 2.0 - m_dragDeltaY;
     }
 }
 
diff --git a/src/renderedtarget.h b/src/renderedtarget.h
index 7c5ed0e..7d5d1c8 100644
--- a/src/renderedtarget.h
+++ b/src/renderedtarget.h
@@ -168,6 +168,8 @@ class RenderedTarget : public IRenderedTarget
         bool m_convexHullDirty = true;
         std::vector<QPoint> m_hullPoints;
         bool m_clicked = false; // left mouse button only!
+        double m_dragX = 0;
+        double m_dragY = 0;
         double m_dragDeltaX = 0;
         double m_dragDeltaY = 0;
 };
diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp
index 696268e..77018c8 100644
--- a/test/renderedtarget/renderedtarget_test.cpp
+++ b/test/renderedtarget/renderedtarget_test.cpp
@@ -440,6 +440,7 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     target.setMouseArea(&mouseArea);
 
     emit mouseArea.mouseMoved(1064, 651);
+    target.beforeRedraw();
     ASSERT_EQ(sprite.x(), 64.08);
     ASSERT_EQ(sprite.y(), -6.86);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
@@ -462,6 +463,7 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     ASSERT_FALSE(sprite.dragging());
 
     emit mouseArea.mouseMoved(1064, 651);
+    target.beforeRedraw();
     ASSERT_EQ(sprite.x(), 64.08);
     ASSERT_EQ(sprite.y(), -6.86);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
@@ -482,6 +484,7 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     ASSERT_FALSE(sprite.dragging());
 
     emit mouseArea.mouseMoved(1064, 651);
+    target.beforeRedraw();
     ASSERT_EQ(sprite.x(), 64.08);
     ASSERT_EQ(sprite.y(), -6.86);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
@@ -501,6 +504,7 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     ASSERT_FALSE(sprite.dragging());
 
     emit mouseArea.mouseMoved(1064, 651);
+    target.beforeRedraw();
     ASSERT_EQ(sprite.x(), 64.08);
     ASSERT_EQ(sprite.y(), -6.86);
     ASSERT_EQ(mouseArea.draggedSprite(), nullptr);
@@ -525,6 +529,7 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     emit mouseArea.mouseMoved(1067.8, 649.06);
+    target.beforeRedraw();
     ASSERT_EQ(std::round(sprite.x() * 100) / 100, 61.22);
     ASSERT_EQ(std::round(sprite.y() * 100) / 100, -14.41);
     ASSERT_EQ(mouseArea.draggedSprite(), &target);
@@ -533,6 +538,7 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480));
     EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360));
     emit mouseArea.mouseMoved(1092.47, 605.46);
+    target.beforeRedraw();
     ASSERT_EQ(std::round(sprite.x() * 100) / 100, 68.26);
     ASSERT_EQ(std::round(sprite.y() * 100) / 100, -1.95);
     ASSERT_EQ(mouseArea.draggedSprite(), &target);
@@ -548,7 +554,7 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     anotherSprite.setY(-6.86);
     anotherSprite.setDraggable(true);
     anotherModel.init(&anotherSprite);
-    anotherTarget.setSpriteModel(&model);
+    anotherTarget.setSpriteModel(&anotherModel);
     anotherTarget.setStageScale(3.5);
     anotherTarget.setMouseArea(&mouseArea);
 
@@ -562,7 +568,7 @@ TEST_F(RenderedTargetTest, SpriteDragging)
     ASSERT_EQ(mouseArea.draggedSprite(), &target);
     ASSERT_TRUE(sprite.dragging());
     ASSERT_FALSE(anotherSprite.dragging());
-    EXPECT_CALL(engine, clickTarget(&sprite));
+    EXPECT_CALL(engine, clickTarget(&anotherSprite));
     QCoreApplication::sendEvent(&anotherTarget, &releaseEvent);
 
     // Stop dragging

From 83dccd1724c0c5b572f13b9d62becf68b747164b Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 14 Apr 2024 11:52:48 +0200
Subject: [PATCH 69/70] Use Qt Quick scale for the pen layer

Resolves: #88
---
 src/ProjectPlayer.qml           | 7 ++++++-
 src/penlayer.cpp                | 1 +
 src/penlayerpainter.cpp         | 6 ++----
 test/penlayer/penlayer_test.cpp | 1 +
 4 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml
index 19a18fc..35cd450 100644
--- a/src/ProjectPlayer.qml
+++ b/src/ProjectPlayer.qml
@@ -136,7 +136,12 @@ ProjectScene {
         PenLayer {
             id: projectPenLayer
             engine: loader.engine
-            anchors.fill: parent
+            anchors.top: parent.top
+            anchors.left: parent.left
+            width: stageWidth
+            height: stageHeight
+            scale: stageScale
+            transformOrigin: Item.TopLeft
             visible: !priv.loading
         }
 
diff --git a/src/penlayer.cpp b/src/penlayer.cpp
index af75526..e2b7642 100644
--- a/src/penlayer.cpp
+++ b/src/penlayer.cpp
@@ -13,6 +13,7 @@ PenLayer::PenLayer(QNanoQuickItem *parent) :
 {
     m_fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
     m_fboFormat.setSamples(m_antialiasingEnabled ? 4 : 0);
+    setSmooth(false);
 }
 
 PenLayer::~PenLayer()
diff --git a/src/penlayerpainter.cpp b/src/penlayerpainter.cpp
index 443b4ab..4c6e5f6 100644
--- a/src/penlayerpainter.cpp
+++ b/src/penlayerpainter.cpp
@@ -27,10 +27,8 @@ void PenLayerPainter::paint(QNanoPainter *painter)
     QOpenGLFramebufferObjectFormat format;
     format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
 
-    // Blit the FBO to a temporary FBO first (multisampled FBOs can only be blitted to FBOs with the same size)
-    QOpenGLFramebufferObject tmpFbo(m_fbo->size(), format);
-    QOpenGLFramebufferObject::blitFramebuffer(&tmpFbo, m_fbo);
-    QOpenGLFramebufferObject::blitFramebuffer(targetFbo, &tmpFbo);
+    // Blit the FBO to the item FBO
+    QOpenGLFramebufferObject::blitFramebuffer(targetFbo, m_fbo);
 }
 
 void PenLayerPainter::synchronize(QNanoQuickItem *item)
diff --git a/test/penlayer/penlayer_test.cpp b/test/penlayer/penlayer_test.cpp
index 01ce44b..a1073e0 100644
--- a/test/penlayer/penlayer_test.cpp
+++ b/test/penlayer/penlayer_test.cpp
@@ -44,6 +44,7 @@ TEST_F(PenLayerTest, Constructors)
     PenLayer penLayer2(&penLayer1);
     ASSERT_EQ(penLayer2.parent(), &penLayer1);
     ASSERT_EQ(penLayer2.parentItem(), &penLayer1);
+    ASSERT_FALSE(penLayer1.smooth());
 }
 
 TEST_F(PenLayerTest, Engine)

From b10c34a7c38061088021aea69a643dd74e1f749b Mon Sep 17 00:00:00 2001
From: adazem009 <68537469+adazem009@users.noreply.github.com>
Date: Sun, 14 Apr 2024 12:00:17 +0200
Subject: [PATCH 70/70] Update libscratchcpp to v0.9.1

---
 libscratchcpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libscratchcpp b/libscratchcpp
index 9ec931a..a16fa2b 160000
--- a/libscratchcpp
+++ b/libscratchcpp
@@ -1 +1 @@
-Subproject commit 9ec931a59daf4d7101382a6f91391b06c591badc
+Subproject commit a16fa2b933ffb5acf0ea9714d8db36d5fd8b2a08
