From 648c70f8a09537390bd22c1ab52fbf82adb57c85 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:52:00 +0100 Subject: [PATCH 01/13] Rename IRenderedTarget::loadCostume() to updateCostume() --- src/irenderedtarget.h | 3 +-- src/renderedtarget.cpp | 6 +++--- src/renderedtarget.h | 3 +-- src/spritemodel.cpp | 2 +- src/stagemodel.cpp | 4 ++-- test/mocks/renderedtargetmock.h | 3 +-- test/renderedtarget/renderedtarget_test.cpp | 10 +++++----- test/target_models/spritemodel_test.cpp | 2 +- test/target_models/stagemodel_test.cpp | 6 +++--- 9 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h index d3295ae..4399c04 100644 --- a/src/irenderedtarget.h +++ b/src/irenderedtarget.h @@ -33,8 +33,7 @@ class IRenderedTarget : public QNanoQuickItem virtual void updateDirection(double direction) = 0; virtual void updateRotationStyle(libscratchcpp::Sprite::RotationStyle style) = 0; virtual void updateLayerOrder(int layerOrder) = 0; - - virtual void loadCostume(libscratchcpp::Costume *costume) = 0; + virtual void updateCostume(libscratchcpp::Costume *costume) = 0; virtual void beforeRedraw() = 0; diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 0d3dfd8..84cf655 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -104,7 +104,7 @@ void RenderedTarget::updateLayerOrder(int layerOrder) setZ(layerOrder); } -void RenderedTarget::loadCostume(Costume *costume) +void RenderedTarget::updateCostume(Costume *costume) { if (!costume || costume == m_costume) return; @@ -189,7 +189,7 @@ void RenderedTarget::setStageModel(StageModel *newStageModel) Stage *stage = m_stageModel->stage(); if (stage) - loadCostume(stage->currentCostume().get()); + updateCostume(stage->currentCostume().get()); } emit stageModelChanged(); @@ -216,7 +216,7 @@ void RenderedTarget::setSpriteModel(SpriteModel *newSpriteModel) m_size = sprite->size() / 100; m_direction = sprite->direction(); m_rotationStyle = sprite->rotationStyle(); - loadCostume(sprite->currentCostume().get()); + updateCostume(sprite->currentCostume().get()); updateVisibility(sprite->visible()); updateLayerOrder(sprite->layerOrder()); calculateSize(); diff --git a/src/renderedtarget.h b/src/renderedtarget.h index 8cb92e9..580278e 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -38,8 +38,7 @@ class RenderedTarget : public IRenderedTarget void updateDirection(double direction) override; void updateRotationStyle(libscratchcpp::Sprite::RotationStyle style) override; void updateLayerOrder(int layerOrder) override; - - void loadCostume(libscratchcpp::Costume *costume) override; + void updateCostume(libscratchcpp::Costume *costume) override; void beforeRedraw() override; diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp index fbd5cca..66e9d41 100644 --- a/src/spritemodel.cpp +++ b/src/spritemodel.cpp @@ -38,7 +38,7 @@ void SpriteModel::onCloned(libscratchcpp::Sprite *clone) void SpriteModel::onCostumeChanged(libscratchcpp::Costume *costume) { if (m_renderedTarget) - m_renderedTarget->loadCostume(costume); + m_renderedTarget->updateCostume(costume); } void SpriteModel::onVisibleChanged(bool visible) diff --git a/src/stagemodel.cpp b/src/stagemodel.cpp index 14521a9..92b17b5 100644 --- a/src/stagemodel.cpp +++ b/src/stagemodel.cpp @@ -20,7 +20,7 @@ void StageModel::init(libscratchcpp::Stage *stage) void StageModel::onCostumeChanged(libscratchcpp::Costume *costume) { if (m_renderedTarget) - m_renderedTarget->loadCostume(costume); + m_renderedTarget->updateCostume(costume); } void StageModel::onTempoChanged(int tempo) @@ -47,7 +47,7 @@ void StageModel::loadCostume() { if (m_renderedTarget && m_stage) { if (m_stage) - m_renderedTarget->loadCostume(m_stage->currentCostume().get()); + m_renderedTarget->updateCostume(m_stage->currentCostume().get()); } } diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h index ab7cf2c..019e70c 100644 --- a/test/mocks/renderedtargetmock.h +++ b/test/mocks/renderedtargetmock.h @@ -19,11 +19,10 @@ class RenderedTargetMock : public IRenderedTarget MOCK_METHOD(void, updateDirection, (double), (override)); MOCK_METHOD(void, updateRotationStyle, (libscratchcpp::Sprite::RotationStyle), (override)); MOCK_METHOD(void, updateLayerOrder, (int), (override)); + MOCK_METHOD(void, updateCostume, (libscratchcpp::Costume *), (override)); MOCK_METHOD(void, beforeRedraw, (), (override)); - MOCK_METHOD(void, loadCostume, (libscratchcpp::Costume *), (override)); - MOCK_METHOD(void, deinitClone, (), (override)); MOCK_METHOD(libscratchcpp::IEngine *, engine, (), (const, override)); diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index 46ea4a2..e541146 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -70,7 +70,7 @@ TEST_F(RenderedTargetTest, UpdateMethods) EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); - target.loadCostume(&costume); + target.updateCostume(&costume); target.beforeRedraw(); ASSERT_EQ(target.width(), 1.6); ASSERT_EQ(target.height(), 2.4); @@ -194,7 +194,7 @@ TEST_F(RenderedTargetTest, LoadJpegCostume) costume.setBitmapResolution(3); RenderedTarget target; - target.loadCostume(&costume); + target.updateCostume(&costume); ASSERT_FALSE(target.isSvg()); ASSERT_FALSE(target.bitmapBuffer()->isOpen()); target.bitmapBuffer()->open(QBuffer::ReadOnly); @@ -210,7 +210,7 @@ TEST_F(RenderedTargetTest, LoadPngCostume) costume.setBitmapResolution(3); RenderedTarget target; - target.loadCostume(&costume); + target.updateCostume(&costume); ASSERT_FALSE(target.isSvg()); ASSERT_FALSE(target.bitmapBuffer()->isOpen()); target.bitmapBuffer()->open(QBuffer::ReadOnly); @@ -258,7 +258,7 @@ TEST_F(RenderedTargetTest, LoadSvgCostume) EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.setSpriteModel(&model); - target.loadCostume(costume.get()); + target.updateCostume(costume.get()); target.beforeRedraw(); ASSERT_TRUE(target.isSvg()); ASSERT_FALSE(target.bitmapBuffer()->isOpen()); @@ -324,7 +324,7 @@ TEST_F(RenderedTargetTest, PaintSvg) RenderedTarget target; target.setEngine(&engine); target.setSpriteModel(&model); - target.loadCostume(&costume); + target.updateCostume(&costume); target.beforeRedraw(); // Create OpenGL context diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp index 17e9df2..6c2ca19 100644 --- a/test/target_models/spritemodel_test.cpp +++ b/test/target_models/spritemodel_test.cpp @@ -87,7 +87,7 @@ TEST(SpriteModelTest, OnCostumeChanged) RenderedTargetMock renderedTarget; model.setRenderedTarget(&renderedTarget); - EXPECT_CALL(renderedTarget, loadCostume(&costume)); + EXPECT_CALL(renderedTarget, updateCostume(&costume)); model.onCostumeChanged(&costume); } diff --git a/test/target_models/stagemodel_test.cpp b/test/target_models/stagemodel_test.cpp index 822e344..a789a5e 100644 --- a/test/target_models/stagemodel_test.cpp +++ b/test/target_models/stagemodel_test.cpp @@ -34,7 +34,7 @@ TEST(StageModelTest, OnCostumeChanged) RenderedTargetMock renderedTarget; model.setRenderedTarget(&renderedTarget); - EXPECT_CALL(renderedTarget, loadCostume(&costume)); + EXPECT_CALL(renderedTarget, updateCostume(&costume)); model.onCostumeChanged(&costume); } @@ -55,12 +55,12 @@ TEST(StageModelTest, RenderedTarget) RenderedTargetMock renderedTarget; QSignalSpy spy(&model, &StageModel::renderedTargetChanged); - EXPECT_CALL(renderedTarget, loadCostume(c2.get())); + EXPECT_CALL(renderedTarget, updateCostume(c2.get())); model.setRenderedTarget(&renderedTarget); ASSERT_EQ(spy.count(), 1); ASSERT_EQ(model.renderedTarget(), &renderedTarget); - EXPECT_CALL(renderedTarget, loadCostume(c3.get())); + EXPECT_CALL(renderedTarget, updateCostume(c3.get())); stage.setCostumeIndex(2); model.loadCostume(); } From c2787bb298ffb34e5bcc7ba35ddec7477b1b02ba Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 20 Jan 2024 10:57:22 +0100 Subject: [PATCH 02/13] Disable multisampling in QNanoPainter --- thirdparty/libqnanopainter/qnanoquickitempainter.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/thirdparty/libqnanopainter/qnanoquickitempainter.cpp b/thirdparty/libqnanopainter/qnanoquickitempainter.cpp index a2041dd..170e344 100644 --- a/thirdparty/libqnanopainter/qnanoquickitempainter.cpp +++ b/thirdparty/libqnanopainter/qnanoquickitempainter.cpp @@ -154,7 +154,6 @@ QOpenGLFramebufferObject *QNanoQuickItemPainter::createFramebufferObject(const Q { QOpenGLFramebufferObjectFormat format; format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); - format.setSamples(16); QSize fboSize(size); if (m_textureWidth > -1) fboSize.setWidth(static_cast(m_textureWidth*m_itemData.devicePixelRatio)); if (m_textureHeight > -1) fboSize.setHeight(static_cast(m_textureHeight*m_itemData.devicePixelRatio)); From 1adbddb953ef85456d8b78de988fb00ba1c05eb8 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 20 Jan 2024 12:57:51 +0100 Subject: [PATCH 03/13] Add Texture class --- src/CMakeLists.txt | 2 + src/texture.cpp | 131 ++++++++++++++++++++++++++++++++++ src/texture.h | 36 ++++++++++ test/CMakeLists.txt | 1 + test/texture/CMakeLists.txt | 14 ++++ test/texture/texture_test.cpp | 121 +++++++++++++++++++++++++++++++ 6 files changed, 305 insertions(+) create mode 100644 src/texture.cpp create mode 100644 src/texture.h create mode 100644 test/texture/CMakeLists.txt create mode 100644 test/texture/texture_test.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5cd33b5..405244b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -31,6 +31,8 @@ qt_add_qml_module(scratchcpp-render listmonitorlistmodel.cpp listmonitorlistmodel.h irenderedtarget.h + texture.cpp + texture.h renderedtarget.cpp renderedtarget.h targetpainter.cpp diff --git a/src/texture.cpp b/src/texture.cpp new file mode 100644 index 0000000..a0bf2da --- /dev/null +++ b/src/texture.cpp @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "texture.h" + +using namespace scratchcpprender; + +Texture::Texture() : + m_size(QSize(0, 0)) +{ +} + +Texture::Texture(GLuint texture, const QSize &size) : + m_handle(texture), + m_isValid(true), + m_size(size) +{ +} + +Texture::Texture(GLuint texture, int width, int height) : + Texture(texture, QSize(width, height)) +{ +} + +GLuint Texture::handle() const +{ + return m_handle; +} + +bool Texture::isValid() const +{ + return m_isValid; +} + +const QSize &Texture::size() const +{ + return m_size; +} + +int Texture::width() const +{ + return m_size.width(); +} + +int Texture::height() const +{ + return m_size.height(); +} + +QImage Texture::toImage() const +{ + if (!m_isValid) + return QImage(); + + QOpenGLContext *context = QOpenGLContext::currentContext(); + + if (!context || !context->isValid()) + return QImage(); + + QOpenGLExtraFunctions glF(context); + glF.initializeOpenGLFunctions(); + + // Create offscreen surface + QOffscreenSurface surface; + surface.setFormat(context->format()); + surface.create(); + Q_ASSERT(surface.isValid()); + + // Save old surface + QSurface *oldSurface = context->surface(); + + // Make context active on the surface + context->makeCurrent(&surface); + + const QRectF drawRect(0, 0, m_size.width(), m_size.height()); + const QSize drawRectSize = drawRect.size().toSize(); + + // Create FBO, but attach the texture to it + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + + QOpenGLFramebufferObject fbo(drawRectSize, format); + + // Create a custom FBO with the texture + unsigned int textureFbo; + glF.glGenFramebuffers(1, &textureFbo); + glF.glBindFramebuffer(GL_FRAMEBUFFER, textureFbo); + glF.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_handle, 0); + + if (glF.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + qWarning() << "error: framebuffer incomplete when generating texture image"; + glF.glDeleteFramebuffers(1, &textureFbo); + return QImage(); + } + + // Blit the FBO to the Qt FBO + glF.glBindFramebuffer(GL_READ_FRAMEBUFFER, textureFbo); + glF.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo.handle()); + glF.glBlitFramebuffer(0, 0, m_size.width(), m_size.height(), 0, 0, fbo.width(), fbo.height(), GL_COLOR_BUFFER_BIT, GL_NEAREST); + glF.glBindFramebuffer(GL_FRAMEBUFFER, 0); + + glF.glDeleteFramebuffers(1, &textureFbo); + + // Get the image + QImage image = fbo.toImage(); + + // Restore old surface + context->doneCurrent(); + + if (oldSurface) + context->makeCurrent(oldSurface); + + return image; +} + +void Texture::release() +{ + if (m_isValid) { + glDeleteTextures(1, &m_handle); + m_isValid = false; + } +} + +bool Texture::operator==(const Texture &texture) const +{ + return (!m_isValid && !texture.m_isValid) || (m_isValid && texture.m_isValid && m_handle == texture.m_handle); +} + +bool scratchcpprender::Texture::operator!=(const Texture &texture) const +{ + return !(*this == texture); +} diff --git a/src/texture.h b/src/texture.h new file mode 100644 index 0000000..894352a --- /dev/null +++ b/src/texture.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +namespace scratchcpprender +{ + +class Texture +{ + public: + Texture(); + Texture(GLuint texture, const QSize &size); + Texture(GLuint texture, int width, int height); + + GLuint handle() const; + bool isValid() const; + const QSize &size() const; + int width() const; + int height() const; + + QImage toImage() const; + + void release(); + + bool operator==(const Texture &texture) const; + bool operator!=(const Texture &texture) const; + + private: + GLuint m_handle = 0; + bool m_isValid = false; + QSize m_size; +}; + +} // namespace scratchcpprender diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 20be59f..12db573 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,3 +29,4 @@ add_subdirectory(keyeventhandler) add_subdirectory(mouseeventhandler) add_subdirectory(scenemousearea) add_subdirectory(monitor_models) +add_subdirectory(texture) diff --git a/test/texture/CMakeLists.txt b/test/texture/CMakeLists.txt new file mode 100644 index 0000000..18d74a5 --- /dev/null +++ b/test/texture/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable( + texture_test + texture_test.cpp +) + +target_link_libraries( + texture_test + GTest::gtest_main + scratchcpp-render + ${QT_LIBS} +) + +add_test(texture_test) +gtest_discover_tests(texture_test) diff --git a/test/texture/texture_test.cpp b/test/texture/texture_test.cpp new file mode 100644 index 0000000..ffdff19 --- /dev/null +++ b/test/texture/texture_test.cpp @@ -0,0 +1,121 @@ +#include + +#include "../common.h" + +using namespace scratchcpprender; + +TEST(TextureTest, Constructors) +{ + { + Texture tex; + ASSERT_EQ(tex.handle(), 0); + ASSERT_FALSE(tex.isValid()); + ASSERT_EQ(tex.size().width(), 0); + ASSERT_EQ(tex.size().height(), 0); + ASSERT_EQ(tex.width(), 0); + ASSERT_EQ(tex.height(), 0); + } + + { + Texture tex(2, QSize(4, 2)); + ASSERT_EQ(tex.handle(), 2); + ASSERT_TRUE(tex.isValid()); + ASSERT_EQ(tex.size().width(), 4); + ASSERT_EQ(tex.size().height(), 2); + ASSERT_EQ(tex.width(), 4); + ASSERT_EQ(tex.height(), 2); + } + + { + Texture tex(2, 5, 8); + ASSERT_EQ(tex.handle(), 2); + ASSERT_TRUE(tex.isValid()); + ASSERT_EQ(tex.size().width(), 5); + ASSERT_EQ(tex.size().height(), 8); + ASSERT_EQ(tex.width(), 5); + ASSERT_EQ(tex.height(), 8); + } +} + +TEST(TextureTest, ToImage) +{ + QOpenGLContext context; + context.create(); + ASSERT_TRUE(context.isValid()); + + QOffscreenSurface surface; + surface.setFormat(context.format()); + surface.create(); + Q_ASSERT(surface.isValid()); + context.makeCurrent(&surface); + + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + + QOpenGLFramebufferObject fbo(80, 60, format); + fbo.bind(); + + QOpenGLPaintDevice device(fbo.size()); + QPainter painter(&device); + painter.beginNativePainting(); + painter.setRenderHint(QPainter::Antialiasing, false); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + painter.drawEllipse(0, 0, fbo.width(), fbo.height()); + painter.endNativePainting(); + painter.end(); + + QImage image = fbo.toImage(); + + Texture tex(fbo.takeTexture(), fbo.width(), fbo.height()); + ASSERT_EQ(tex.toImage(), image); + + tex.release(); + context.doneCurrent(); +} + +TEST(TextureTest, Release) +{ + QOpenGLContext context; + context.create(); + ASSERT_TRUE(context.isValid()); + + QOffscreenSurface surface; + surface.setFormat(context.format()); + surface.create(); + Q_ASSERT(surface.isValid()); + context.makeCurrent(&surface); + + QOpenGLFramebufferObject fbo(1, 1); + GLuint handle = fbo.takeTexture(); + ASSERT_TRUE(glIsTexture(handle)); + + Texture tex(handle, fbo.width(), fbo.height()); + ASSERT_TRUE(glIsTexture(handle)); + + tex.release(); + ASSERT_FALSE(glIsTexture(handle)); + ASSERT_FALSE(tex.isValid()); + + context.doneCurrent(); +} + +TEST(TextureTest, Operators) +{ + Texture t1; + Texture t2; + ASSERT_TRUE(t1 == t2); + ASSERT_FALSE(t1 != t2); + + Texture t3(3, 10, 10); + ASSERT_FALSE(t1 == t3); + ASSERT_TRUE(t1 != t3); + + Texture t4(3, 10, 10); + ASSERT_TRUE(t3 == t4); + ASSERT_FALSE(t3 != t4); + + Texture t5(2, 10, 10); + ASSERT_FALSE(t4 == t5); + ASSERT_TRUE(t4 != t5); +} From 5818643fc9b176d860b6eceecf30858e65f550c1 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:06:26 +0100 Subject: [PATCH 04/13] Add Skin class --- src/CMakeLists.txt | 2 ++ src/skin.cpp | 90 ++++++++++++++++++++++++++++++++++++++++++++++ src/skin.h | 36 +++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 src/skin.cpp create mode 100644 src/skin.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 405244b..67dba9c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,6 +33,8 @@ qt_add_qml_module(scratchcpp-render irenderedtarget.h texture.cpp texture.h + skin.cpp + skin.h renderedtarget.cpp renderedtarget.h targetpainter.cpp diff --git a/src/skin.cpp b/src/skin.cpp new file mode 100644 index 0000000..61d92cb --- /dev/null +++ b/src/skin.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include "skin.h" +#include "texture.h" + +using namespace scratchcpprender; + +Skin::Skin(libscratchcpp::Costume *costume) +{ + if (!costume) + return; +} + +Texture Skin::createAndPaintTexture(int width, int height, bool multisampled) +{ + QOpenGLContext *context = QOpenGLContext::currentContext(); + + if (!context || !context->isValid() || (width <= 0 || height <= 0)) + return Texture(); + + // Create offscreen surface + QOffscreenSurface surface; + surface.setFormat(context->format()); + surface.create(); + Q_ASSERT(surface.isValid()); + + // Save old surface + QSurface *oldSurface = context->surface(); + + // Make context active on the surface + context->makeCurrent(&surface); + + const QRectF drawRect(0, 0, width, height); + const QSize drawRectSize = drawRect.size().toSize(); + + // Create multisampled FBO (if the multisampled parameter is set) + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + + if (multisampled) + format.setSamples(16); + + QOpenGLFramebufferObject fbo(drawRectSize, format); + fbo.bind(); + + // Create paint device + QOpenGLPaintDevice device(drawRectSize); + QPainter painter(&device); + painter.beginNativePainting(); + painter.setRenderHint(QPainter::Antialiasing, false); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + + // Call the skin-specific paint method + paint(&painter); + + // Done with the painting + painter.endNativePainting(); + painter.end(); + fbo.release(); + + GLuint textureHandle; + + if (multisampled) { + // Create non-multisampled FBO (we can't take the texture from the multisampled FBO) + format.setSamples(0); + + QOpenGLFramebufferObject targetFbo(drawRectSize, format); + targetFbo.bind(); + + // Blit the multisampled FBO to target FBO + QOpenGLFramebufferObject::blitFramebuffer(&targetFbo, &fbo); + + // Take the texture (will call targetFbo.release()) + textureHandle = targetFbo.takeTexture(); + } else { + // Take the texture + textureHandle = fbo.takeTexture(); + } + + // Restore old surface + context->doneCurrent(); + + if (oldSurface) + context->makeCurrent(oldSurface); + + return Texture(textureHandle, drawRectSize); +} diff --git a/src/skin.h b/src/skin.h new file mode 100644 index 0000000..54096f7 --- /dev/null +++ b/src/skin.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include +#include +#include + +namespace libscratchcpp +{ + +class Costume; + +} + +namespace scratchcpprender +{ + +class Texture; + +class Skin +{ + public: + Skin(libscratchcpp::Costume *costume); + Skin(const Skin &) = delete; + virtual ~Skin() { } + + virtual Texture getTexture(double scale) const = 0; + virtual double getTextureScale(const Texture &texture) const = 0; + + protected: + Texture createAndPaintTexture(int width, int height, bool multisampled); + virtual void paint(QPainter *painter) = 0; +}; + +} // namespace scratchcpprender From 3db89da7d61ee06dae0614699c484fc7626c3075 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:12:31 +0100 Subject: [PATCH 05/13] Add BitmapSkin class --- src/CMakeLists.txt | 2 + src/bitmapskin.cpp | 55 ++++++++++++++++++++++++ src/bitmapskin.h | 29 +++++++++++++ test/CMakeLists.txt | 1 + test/jpeg_result.png | Bin 0 -> 157 bytes test/png_result.png | Bin 0 -> 127 bytes test/skins/CMakeLists.txt | 14 +++++++ test/skins/bitmapskin_test.cpp | 74 +++++++++++++++++++++++++++++++++ 8 files changed, 175 insertions(+) create mode 100644 src/bitmapskin.cpp create mode 100644 src/bitmapskin.h create mode 100644 test/jpeg_result.png create mode 100644 test/png_result.png create mode 100644 test/skins/CMakeLists.txt create mode 100644 test/skins/bitmapskin_test.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 67dba9c..b940e96 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -35,6 +35,8 @@ qt_add_qml_module(scratchcpp-render texture.h skin.cpp skin.h + bitmapskin.cpp + bitmapskin.h renderedtarget.cpp renderedtarget.h targetpainter.cpp diff --git a/src/bitmapskin.cpp b/src/bitmapskin.cpp new file mode 100644 index 0000000..a3adb44 --- /dev/null +++ b/src/bitmapskin.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include "bitmapskin.h" + +using namespace scratchcpprender; + +BitmapSkin::BitmapSkin(libscratchcpp::Costume *costume) : + Skin(costume) +{ + if (!costume) + return; + + // Read image data + QBuffer buffer; + buffer.open(QBuffer::WriteOnly); + buffer.write(static_cast(costume->data()), costume->dataSize()); + buffer.close(); + const char *format; + + { + QImageReader reader(&buffer); + format = reader.format(); + } + + buffer.close(); + m_image.load(&buffer, format); + + // Paint the image into a texture + m_texture = createAndPaintTexture(m_image.width(), m_image.height(), false); + m_textureSize.setWidth(m_image.width()); + m_textureSize.setHeight(m_image.height()); + Q_ASSERT(m_texture.isValid()); +} + +BitmapSkin::~BitmapSkin() +{ + m_texture.release(); +} + +Texture BitmapSkin::getTexture(double scale) const +{ + return m_texture; +} + +double BitmapSkin::getTextureScale(const Texture &texture) const +{ + return 1; +} + +void BitmapSkin::paint(QPainter *painter) +{ + painter->drawImage(m_image.rect(), m_image, m_image.rect()); +} diff --git a/src/bitmapskin.h b/src/bitmapskin.h new file mode 100644 index 0000000..0979ae1 --- /dev/null +++ b/src/bitmapskin.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include "skin.h" +#include "texture.h" + +namespace scratchcpprender +{ + +class BitmapSkin : public Skin +{ + public: + BitmapSkin(libscratchcpp::Costume *costume); + ~BitmapSkin(); + + Texture getTexture(double scale) const override; + double getTextureScale(const Texture &texture) const override; + + protected: + void paint(QPainter *painter) override; + + private: + Texture m_texture; + QSize m_textureSize; + QImage m_image; +}; + +} // namespace scratchcpprender diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 12db573..7be095e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -30,3 +30,4 @@ add_subdirectory(mouseeventhandler) add_subdirectory(scenemousearea) add_subdirectory(monitor_models) add_subdirectory(texture) +add_subdirectory(skins) diff --git a/test/jpeg_result.png b/test/jpeg_result.png new file mode 100644 index 0000000000000000000000000000000000000000..9123a6043a52e4767d86ed868adae67376dcd142 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c0!3HFsSlX8YDb50q$YKTtz9S&aI8~cZ8Yt-R z>Eal|F*EkyM&3gP0t^R?y}Iv2nk-zjEkma<#Vui?&V;K8!oTF7WD1tIRv!{cXuKA7 z`)lSo&#Rx`Ozivgw=#8QRp04?+x|*Uj=u^&Jm-GBOEAOwbJ7~1(F~rhelF{r5}E*n CYd6IJ literal 0 HcmV?d00001 diff --git a/test/png_result.png b/test/png_result.png new file mode 100644 index 0000000000000000000000000000000000000000..0b6a90efb7f11a257b99612fee72564d2c8c40fc GIT binary patch literal 127 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c0!3HFsSlX8YDb50q$YKTtz9S&aI8~cZ8YpP! z>Eal|F*CPkBQJvi$KjqUhYt(J+*Cf)uxmlp#wQ0ibWF@;c>3DNVdpD`j9K}|>}oPj X_wlT8xzSJn)Xd=N>gTe~DWM4f#Puh} literal 0 HcmV?d00001 diff --git a/test/skins/CMakeLists.txt b/test/skins/CMakeLists.txt new file mode 100644 index 0000000..25ea2ee --- /dev/null +++ b/test/skins/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable( + bitmapskin_test + bitmapskin_test.cpp +) + +target_link_libraries( + bitmapskin_test + GTest::gtest_main + scratchcpp-render + ${QT_LIBS} +) + +add_test(bitmapskin_test) +gtest_discover_tests(bitmapskin_test) diff --git a/test/skins/bitmapskin_test.cpp b/test/skins/bitmapskin_test.cpp new file mode 100644 index 0000000..ab6eb17 --- /dev/null +++ b/test/skins/bitmapskin_test.cpp @@ -0,0 +1,74 @@ +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +class BitmapSkinTest : public testing::Test +{ + public: + void SetUp() override + { + m_context.create(); + ASSERT_TRUE(m_context.isValid()); + + m_surface.setFormat(m_context.format()); + m_surface.create(); + Q_ASSERT(m_surface.isValid()); + m_context.makeCurrent(&m_surface); + + Costume jpegCostume("", "", ""); + std::string costumeData = readFileStr("image.jpg"); + jpegCostume.setData(costumeData.size(), costumeData.data()); + m_jpegSkin = std::make_unique(&jpegCostume); + + Costume pngCostume("", "", ""); + costumeData = readFileStr("image.png"); + pngCostume.setData(costumeData.size(), costumeData.data()); + m_pngSkin = std::make_unique(&pngCostume); + } + + void TearDown() override + { + ASSERT_EQ(m_context.surface(), &m_surface); + m_context.doneCurrent(); + } + + QOpenGLContext m_context; + QOffscreenSurface m_surface; + std::unique_ptr m_jpegSkin; + std::unique_ptr m_pngSkin; +}; + +TEST_F(BitmapSkinTest, GetTexture) +{ + Texture texture = m_jpegSkin->getTexture(1); + ASSERT_EQ(texture.width(), 4); + ASSERT_EQ(texture.height(), 6); + + QBuffer jpegBuffer; + texture.toImage().save(&jpegBuffer, "png"); + QFile jpegRef("jpeg_result.png"); + jpegRef.open(QFile::ReadOnly); + jpegBuffer.open(QBuffer::ReadOnly); + ASSERT_EQ(jpegBuffer.readAll(), jpegRef.readAll()); + + texture = m_pngSkin->getTexture(1); + ASSERT_EQ(texture.width(), 4); + ASSERT_EQ(texture.height(), 6); + + QBuffer pngBuffer; + texture.toImage().save(&pngBuffer, "png"); + QFile pngRef("png_result.png"); + pngRef.open(QFile::ReadOnly); + pngBuffer.open(QBuffer::ReadOnly); + ASSERT_EQ(pngBuffer.readAll(), pngRef.readAll()); +} + +TEST_F(BitmapSkinTest, GetTextureScale) +{ + ASSERT_EQ(m_jpegSkin->getTextureScale(Texture()), 1); + ASSERT_EQ(m_pngSkin->getTextureScale(Texture()), 1); +} From 550d5288cce3d372dc23b6a8aeaa0e831d9b4016 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:40:57 +0100 Subject: [PATCH 06/13] Add SVGSkin class --- src/CMakeLists.txt | 2 + src/svgskin.cpp | 103 ++++++++++++++++++++++++++++++++ src/svgskin.h | 36 +++++++++++ test/skins/CMakeLists.txt | 17 ++++++ test/skins/svgskin_test.cpp | 73 ++++++++++++++++++++++ test/svg_texture_results/10.png | Bin 0 -> 245 bytes test/svg_texture_results/11.png | Bin 0 -> 454 bytes test/svg_texture_results/14.png | Bin 0 -> 5212 bytes test/svg_texture_results/5.png | Bin 0 -> 91 bytes test/svg_texture_results/6.png | Bin 0 -> 108 bytes test/svg_texture_results/7.png | Bin 0 -> 121 bytes test/svg_texture_results/8.png | Bin 0 -> 137 bytes test/svg_texture_results/9.png | Bin 0 -> 167 bytes 13 files changed, 231 insertions(+) create mode 100644 src/svgskin.cpp create mode 100644 src/svgskin.h create mode 100644 test/skins/svgskin_test.cpp create mode 100644 test/svg_texture_results/10.png create mode 100644 test/svg_texture_results/11.png create mode 100644 test/svg_texture_results/14.png create mode 100644 test/svg_texture_results/5.png create mode 100644 test/svg_texture_results/6.png create mode 100644 test/svg_texture_results/7.png create mode 100644 test/svg_texture_results/8.png create mode 100644 test/svg_texture_results/9.png diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b940e96..bb1993a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,6 +37,8 @@ qt_add_qml_module(scratchcpp-render skin.h bitmapskin.cpp bitmapskin.h + svgskin.cpp + svgskin.h renderedtarget.cpp renderedtarget.h targetpainter.cpp diff --git a/src/svgskin.cpp b/src/svgskin.cpp new file mode 100644 index 0000000..bc6576c --- /dev/null +++ b/src/svgskin.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include "svgskin.h" + +using namespace scratchcpprender; + +static const int MAX_TEXTURE_DIMENSION = 2048; +static const int INDEX_OFFSET = 8; + +SVGSkin::SVGSkin(libscratchcpp::Costume *costume, bool antialiasing) : + Skin(costume), + m_antialiasing(antialiasing) +{ + if (!costume) + return; + + // Load SVG data + m_svgRen.load(QByteArray(static_cast(costume->data()), costume->dataSize())); + + // Calculate maximum index (larger images will only be scaled up) + const QRectF viewBox = m_svgRen.viewBox(); + + if (viewBox.width() == 0 || viewBox.height() == 0) + return; + + 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() +{ + for (const auto &[index, texture] : m_textures) + m_textureObjects[texture].release(); +} + +Texture SVGSkin::getTexture(double scale) const +{ + // https://github.com/scratchfoundation/scratch-render/blob/423bb700c36b8c1c0baae1e2413878a4f778849a/src/SVGSkin.js#L158-L176 + int mipLevel = std::max(std::ceil(std::log2(scale)) + INDEX_OFFSET, 0.0); + + // Limit to maximum index + mipLevel = std::min(mipLevel, m_maxIndex); + + auto it = m_textures.find(mipLevel); + + if (it == m_textures.cend()) + return const_cast(this)->createScaledTexture(mipLevel); // TODO: Remove that awful const_cast ;) + else + return m_textureObjects.at(it->second); +} + +double SVGSkin::getTextureScale(const Texture &texture) const +{ + auto it = m_textureIndexes.find(texture.handle()); + + if (it != m_textureIndexes.cend()) + return std::pow(2, it->second - INDEX_OFFSET); + + return 1; +} + +void SVGSkin::paint(QPainter *painter) +{ + const QPaintDevice *device = painter->device(); + m_svgRen.render(painter, QRectF(0, 0, device->width(), device->height())); +} + +Texture SVGSkin::createScaledTexture(int index) +{ + Q_ASSERT(m_textures.find(index) == m_textures.cend()); + auto it = m_textures.find(index); + + if (it != m_textures.cend()) + return m_textureObjects[it->second]; + + const double scale = std::pow(2, index - INDEX_OFFSET); + const QRect viewBox = m_svgRen.viewBox(); + const double width = viewBox.width() * scale; + const double height = viewBox.height() * scale; + + if (width > MAX_TEXTURE_DIMENSION || height > MAX_TEXTURE_DIMENSION) { + Q_ASSERT(false); // this shouldn't happen because indexes are limited to the max index + return Texture(); + } + + const Texture texture = createAndPaintTexture(viewBox.width() * scale, viewBox.height() * scale, m_antialiasing); + + if (texture.isValid()) { + m_textures[index] = texture.handle(); + m_textureIndexes[texture.handle()] = index; + m_textureObjects[texture.handle()] = texture; + } + + return texture; +} diff --git a/src/svgskin.h b/src/svgskin.h new file mode 100644 index 0000000..95c0768 --- /dev/null +++ b/src/svgskin.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +#include "skin.h" +#include "texture.h" + +namespace scratchcpprender +{ + +class SVGSkin : public Skin +{ + public: + SVGSkin(libscratchcpp::Costume *costume, bool antialiasing = true); + ~SVGSkin(); + + Texture getTexture(double scale) const override; + double getTextureScale(const Texture &texture) const override; + + protected: + void paint(QPainter *painter) override; + + private: + Texture createScaledTexture(int index); + + std::unordered_map m_textures; + std::unordered_map m_textureIndexes; // reverse map of m_textures + std::unordered_map m_textureObjects; + QSvgRenderer m_svgRen; + int m_maxIndex = 0; + bool m_antialiasing = false; +}; + +} // namespace scratchcpprender diff --git a/test/skins/CMakeLists.txt b/test/skins/CMakeLists.txt index 25ea2ee..34f8ece 100644 --- a/test/skins/CMakeLists.txt +++ b/test/skins/CMakeLists.txt @@ -1,3 +1,4 @@ +# bitmapskin add_executable( bitmapskin_test bitmapskin_test.cpp @@ -12,3 +13,19 @@ target_link_libraries( add_test(bitmapskin_test) gtest_discover_tests(bitmapskin_test) + +# svgskin +add_executable( + svgskin_test + svgskin_test.cpp +) + +target_link_libraries( + svgskin_test + GTest::gtest_main + scratchcpp-render + ${QT_LIBS} +) + +add_test(svgskin_test) +gtest_discover_tests(svgskin_test) diff --git a/test/skins/svgskin_test.cpp b/test/skins/svgskin_test.cpp new file mode 100644 index 0000000..d028d38 --- /dev/null +++ b/test/skins/svgskin_test.cpp @@ -0,0 +1,73 @@ +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +class SVGSkinTest : public testing::Test +{ + public: + void SetUp() override + { + m_context.create(); + ASSERT_TRUE(m_context.isValid()); + + m_surface.setFormat(m_context.format()); + m_surface.create(); + Q_ASSERT(m_surface.isValid()); + m_context.makeCurrent(&m_surface); + + Costume costume("", "", ""); + std::string costumeData = readFileStr("image.svg"); + costume.setData(costumeData.size(), costumeData.data()); + m_skin = std::make_unique(&costume, false); + } + + void TearDown() override + { + ASSERT_EQ(m_context.surface(), &m_surface); + m_context.doneCurrent(); + } + + QOpenGLContext m_context; + QOffscreenSurface m_surface; + std::unique_ptr m_skin; +}; + +TEST_F(SVGSkinTest, Textures) +{ + static const int INDEX_OFFSET = 8; + + for (int i = 0; i <= 18; i++) { + double scale = std::pow(2, i - INDEX_OFFSET); + Texture texture = m_skin->getTexture(scale); + int dimension = static_cast(13 * scale); + ASSERT_TRUE(texture.isValid() || dimension == 0); + + if (!texture.isValid()) + continue; + + if (i > 15) { + ASSERT_EQ(texture.width(), 1664); + ASSERT_EQ(texture.height(), 1664); + ASSERT_EQ(m_skin->getTextureScale(texture), 128); + } else { + ASSERT_EQ(texture.width(), dimension); + ASSERT_EQ(texture.height(), dimension); + ASSERT_EQ(m_skin->getTextureScale(texture), scale); + } + + // Skip images 12, 13 and 15 because they're different on xvfb + if (i == 12 || i == 13 || i >= 15) + continue; + + QBuffer buffer; + texture.toImage().save(&buffer, "png"); + QFile ref("svg_texture_results/" + QString::number(std::min(i, 15)) + ".png"); + ref.open(QFile::ReadOnly); + buffer.open(QBuffer::ReadOnly); + ASSERT_EQ(buffer.readAll(), ref.readAll()); + } +} diff --git a/test/svg_texture_results/10.png b/test/svg_texture_results/10.png new file mode 100644 index 0000000000000000000000000000000000000000..01436ab045928a8fa8ba61cfc70710f3ac20fbcc GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^CLqkg1|*MGNWTVBoCO|{#S9F5M?jcysy3fAP;j}Y zi(^Pe#BTuI3`bV3u@Bqze zxuaKqeER(RdiCe$=Ed)O|L2Flwf&sqEM3AHehVxb9}&$^02;>7P$=7;5qIu_YA)CahHZI_}{x(M&#ujhw!73e4Qn33syhYVRw1O zUKKV+Rc@v1i<^bh3YO$8=qWm-5Kc?7Ge(yZh9ZK9?uXQu7S53RorK7QK>w>$l zBb~H2LEQLyj^otN>78?&#YEHt<~S5G9p!{EO65Y*t@u|@Y`-GOeG25M9oHmHHvgC5 znrm+?Hbp%k=Dm8}vHy?aHZIultSRk}`S1R`1(MsPkNL;MRXZB%6}dn931f_s xT|@Sd_8->+*jppd@qD?pID*}c5#;Fy=kjBiy-Q~9wH5?%Jzf1=);T3K0RXWTyj1`I literal 0 HcmV?d00001 diff --git a/test/svg_texture_results/14.png b/test/svg_texture_results/14.png new file mode 100644 index 0000000000000000000000000000000000000000..40bb626f66641946ba21e6b8e09c93a3cf552cf7 GIT binary patch literal 5212 zcmeHLeNa=`6~FI;Pw^K%fjFlZfcuJJ5AYgX}w{;z#T3uUIi1-m8U{At=QFnHBI{l+F zYi2SW?)%+yf9KwN&iTFA7iFghjhQe8Arv$}!|(%yD4q9BuDtID75{QJC}{`#2msFKD@jw;vC5->OEsFIp@^%0UI!<(bhhhONj;MFc|QNsQ2^ zj1D2)7DOWS3e6z&zM4ab3nXZSR$>+*HsuBM#SXmKf$z``K!JGi`7d{%p|$0f3h|O4=8*e**!m*hHhOqSRzPeDsz)-fjOOL;O{3VeKs0B2k)n`vXYCRpx(P1QEV zih`j5B`t89r>f~5H6S9K(g;8t1a}2?RhnFn7DD*#jJ8!r6A77!NLb*BRmkWbhM>(c zSDRXa=o>V{Cpx83j2?Bvwsp(2H3pjS{0@=sNv@_}85n*qAeI6`p+~iVsFi7NgF8<$ zB0QNMhu2*Utet+XY!YlnH-N7=rSkxM^imc;Qay`UE20_BtxlzhwIp7x(2ldS?CpvR zTDUmE%c1PPmj0C8A=oC@>9)HG89hb9?o0RZS_8vGa9pU1YLx4BHbl;KN{?sJgbaKz z+MXmCO>Yo^Uis94h=KYcey~Qz-=rDG92Zrg(DP@(jB!!zVE(a)^7d#W$0g=kgzJFY z0uNO#s#&JzSAqEwn1D+g#ceWLgoGT{q#*I=ZMi_N`htJ z@Qh{dl6L_u8%2u%-41Ae1l^ZwF--z=VJM3b+eFf>=liLyI+9Q*bo_CrwB}Ws*j`A2 zYVn2|59m#RP98-!_frE7lGtA7E6|~=X?myX^NJ9>%NF2OH?WPpjv@Xeu%8mG8&x`$ zJh9F^BpO&J5@(K5an)!$@&iaw{fMjRF6n(^30Ly;#OoT%=II4yo*m(^HGhx*|H$|M zuw2R(xUP5$HFPsDxM9}_k&B0ikhY#AUON1wnV3m&UUT2Xo=|X@`t&*%f#C7x@k%o> z_K7Z=X6E}U8;M7SB5`G_Zx8)M+DU?JyD^{=7N4ZZ?3Ki*q`OK7Ybtu@2)S(nx82A+ zt?!!V==k`e$5hd8yE#trbx&RJRC(qd9e>J?pr2N(U{UiuM4D$w)ky3Yo1d~wup(tM zh56isQlxY~%iB+fE)lpD8;D@d-kkzRJxk^}DO%ttspzF=n?n}nQ!2lJznzACGwm@l ztzFi*X*@{dT?=EX(;w5OH~db~kJtabK8`tHL5`BzD<`)ONp#MGJKGOfNXKlyoAa#{ zz3UZtKiKQ;ztaM*4QuXx*7D^AEn%$o-C)1t^gDc?TPRtY@`H|&`VHhn&2ZvfhjaX5#+~4NZagHPvuU-mYEtMvF!>s_Sp;R&7EHqiRt5K z9%-LTUr13;Yu`k(+D8G(=%IoDa%j~cG5u-xVNK(ef+r|!xDNT!xk;joEuX2mRX8LH zT<+r%t>cY3?5(;XMCCq7U`+{i7E0J8;PzE+2H;(rL-50bK_b#LGBKe>=ZNW&#!5zr z+*}@D9W+`v`*1vCnOId6bGxaZ${a1?@wp|YO`R&gLfbNi?t%igeK^6P(EcokHC8+j z?;5Rdz4hd&xiwn;pTlJ$02_hIO`sAwN@Z@1E*7Xb@CcPopwb6aeu-IAVmQ93F6Pca zRPZti5$^$X)dR8deMELnwVISJD*D`LPg)gV?aGCT!`tEQU{OpSbOUb(+sNq6NiJ#) zKZ1UkA7Jf;WwKzIm~dP^t(f}_m|8F+in;S}DTaH+#>3>PX7Cy$VNJ`zaTVaQz^Y~DnaXjqWrl5T;xy<9{E@+fKu-wk75Z5=6(Z=K5ApJXYhq#j zvA*<=p?^Vk3F$fP`jvx(8zS{&(sc?KN}?6&WV1M*(U z8-n-80l+JSci{c;0PxD=*X{f33{w6BSnS`W9YZKlLXL0Kt(47KVljR)zb!l5>G789ZJ6T-G@yGywns_8uMp literal 0 HcmV?d00001 diff --git a/test/svg_texture_results/7.png b/test/svg_texture_results/7.png new file mode 100644 index 0000000000000000000000000000000000000000..5c80b87c7dd090b4186c57c9c4af8de064b651b1 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^Y#_`5A|IT2?*XJZ3p^r=85sDEfH31!Z9ZwBpth%r zV+hC0ypM=l2g!=~*VJt=JV`9N(9 Mp00i_>zopr0FAvNDgXcg literal 0 HcmV?d00001 diff --git a/test/svg_texture_results/8.png b/test/svg_texture_results/8.png new file mode 100644 index 0000000000000000000000000000000000000000..bb6c391c118eb326dc4635175bf24836f7af7055 GIT binary patch literal 137 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4uAB#T}@sR2@)1s;*b3=DinK$vl=HlH+5(Av|* zF+^ixa)JW0!J-fU?DdYX^V%jn^R!E!Z%XDO`Gg}{zn=Vewuo*El#89EW1o1dC5M~u f_KW8>zZe^HpGVji6Q->y+3=~nqF zx+RaZbDChfpq{tKhMXTy7YBBqU@P&y*SsU3_S1jGgEgjHD>xZ=PlzmAzoq6i&=Lku LS3j3^P6 Date: Sat, 20 Jan 2024 22:06:48 +0100 Subject: [PATCH 07/13] Refactor rendering to use skins --- src/irenderedtarget.h | 11 +- src/renderedtarget.cpp | 210 ++++++-------- src/renderedtarget.h | 30 +- src/targetpainter.cpp | 70 +++-- src/targetpainter.h | 4 +- test/mocks/renderedtargetmock.h | 10 +- test/renderedtarget/renderedtarget_test.cpp | 303 ++++++++------------ test/svg_result.png | Bin 436 -> 0 bytes test/targetpainter/targetpainter_test.cpp | 113 +++----- 9 files changed, 319 insertions(+), 432 deletions(-) delete mode 100644 test/svg_result.png diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h index 4399c04..5412a7c 100644 --- a/src/irenderedtarget.h +++ b/src/irenderedtarget.h @@ -2,6 +2,7 @@ #pragma once +#include #include #include @@ -15,6 +16,7 @@ namespace scratchcpprender class StageModel; class SpriteModel; class SceneMouseArea; +class Texture; class IRenderedTarget : public QNanoQuickItem { @@ -35,6 +37,9 @@ class IRenderedTarget : public QNanoQuickItem virtual void updateLayerOrder(int layerOrder) = 0; virtual void updateCostume(libscratchcpp::Costume *costume) = 0; + virtual bool costumesLoaded() const = 0; + virtual void loadCostumes() = 0; + virtual void beforeRedraw() = 0; virtual void deinitClone() = 0; @@ -64,16 +69,12 @@ class IRenderedTarget : public QNanoQuickItem virtual QPointF mapFromScene(const QPointF &point) const = 0; - virtual QBuffer *bitmapBuffer() = 0; - virtual const QString &bitmapUniqueKey() const = 0; - virtual void lockCostume() = 0; virtual void unlockCostume() = 0; virtual bool mirrorHorizontally() const = 0; - virtual bool isSvg() const = 0; - virtual void paintSvg(QNanoPainter *painter) = 0; + virtual Texture texture() const = 0; virtual void updateHullPoints(QOpenGLFramebufferObject *fbo) = 0; virtual const std::vector &hullPoints() const = 0; diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 84cf655..defb114 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -10,6 +10,8 @@ #include "stagemodel.h" #include "spritemodel.h" #include "scenemousearea.h" +#include "bitmapskin.h" +#include "svgskin.h" using namespace scratchcpprender; using namespace libscratchcpp; @@ -19,25 +21,12 @@ static const double SVG_SCALE_LIMIT = 0.1; // the maximum viewport dimensions ar RenderedTarget::RenderedTarget(QNanoQuickItem *parent) : IRenderedTarget(parent) { - // Get maximum viewport dimensions - QOpenGLContext context; - context.create(); - Q_ASSERT(context.isValid()); - - if (context.isValid()) { - QOffscreenSurface surface; - surface.create(); - Q_ASSERT(surface.isValid()); - - if (surface.isValid()) { - context.makeCurrent(&surface); - GLint dims[2]; - glGetIntegerv(GL_MAX_VIEWPORT_DIMS, dims); - m_maximumWidth = dims[0] * SVG_SCALE_LIMIT; - m_maximumHeight = dims[1] * SVG_SCALE_LIMIT; - context.doneCurrent(); - } - } +} + +RenderedTarget::~RenderedTarget() +{ + for (const auto &[costume, skin] : m_skins) + delete skin; } void RenderedTarget::updateVisibility(bool visible) @@ -112,29 +101,13 @@ void RenderedTarget::updateCostume(Costume *costume) m_costumeMutex.lock(); m_costume = costume; - if (m_costume->dataFormat() == "svg") { - m_svgRenderer.load(QByteArray::fromRawData(static_cast(m_costume->data()), m_costume->dataSize())); - QRectF rect = m_svgRenderer.viewBoxF(); - m_costumeWidth = rect.width(); - m_costumeHeight = rect.height(); - } else { - m_bitmapBuffer.open(QBuffer::WriteOnly); - m_bitmapBuffer.write(static_cast(m_costume->data()), m_costume->dataSize()); - m_bitmapBuffer.close(); - m_bitmapUniqueKey = QString::fromStdString(m_costume->id()); - const char *format; - - { - QImageReader reader(&m_bitmapBuffer); - format = reader.format(); - } + if (m_costumesLoaded) { + auto it = m_skins.find(m_costume); - m_bitmapBuffer.close(); - m_costumeBitmap.load(&m_bitmapBuffer, format); - QSize size = m_costumeBitmap.size(); - m_costumeWidth = std::max(0, size.width()); - m_costumeHeight = std::max(0, size.height()); - m_bitmapBuffer.close(); + if (it == m_skins.end()) + m_skin = nullptr; + else + m_skin = it->second; } m_costumeMutex.unlock(); @@ -143,10 +116,59 @@ void RenderedTarget::updateCostume(Costume *costume) calculatePos(); } +bool RenderedTarget::costumesLoaded() const +{ + return m_costumesLoaded; +} + +void RenderedTarget::loadCostumes() +{ + // Delete previous skins + for (const auto &[costume, skin] : m_skins) + delete skin; + + m_skins.clear(); + + // Generate a skin for each costume + Target *target = scratchTarget(); + + if (!target) + return; + + const auto &costumes = target->costumes(); + + for (auto costume : costumes) { + Skin *skin = nullptr; + if (costume->dataFormat() == "svg") + skin = new SVGSkin(costume.get()); + else + skin = new BitmapSkin(costume.get()); + + if (skin) + m_skins[costume.get()] = skin; + + if (m_costume && costume.get() == m_costume) + m_skin = skin; + } + + m_costumesLoaded = true; + + if (m_costume) { + calculateSize(); + calculatePos(); + } +} + void RenderedTarget::beforeRedraw() { + // These properties must be set here to avoid unnecessary calls to update() setWidth(m_width); setHeight(m_height); + + if (!m_oldTexture.isValid() || (m_texture.isValid() && m_texture != m_oldTexture)) { + m_oldTexture = m_texture; + update(); + } } void RenderedTarget::deinitClone() @@ -343,35 +365,9 @@ void RenderedTarget::mouseMoveEvent(QMouseEvent *event) } } -void RenderedTarget::paintSvg(QNanoPainter *painter) +Texture RenderedTarget::texture() const { - Q_ASSERT(painter); - QOpenGLContext *context = QOpenGLContext::currentContext(); - Q_ASSERT(context); - - if (!context) - return; - - QOffscreenSurface surface; - surface.setFormat(context->format()); - surface.create(); - Q_ASSERT(surface.isValid()); - - QSurface *oldSurface = context->surface(); - context->makeCurrent(&surface); - - const QRectF drawRect(0, 0, std::min(width(), m_maximumWidth), std::min(height(), m_maximumHeight)); - const QSize drawRectSize = drawRect.size().toSize(); - - QOpenGLPaintDevice device(drawRectSize); - QPainter qPainter; - qPainter.begin(&device); - qPainter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); - m_svgRenderer.render(&qPainter, drawRect); - qPainter.end(); - - context->doneCurrent(); - context->makeCurrent(oldSurface); + return m_texture; } void RenderedTarget::updateHullPoints(QOpenGLFramebufferObject *fbo) @@ -458,27 +454,24 @@ bool RenderedTarget::contains(const QPointF &point) const void RenderedTarget::calculatePos() { - if (!m_costume || !m_engine) + if (!m_skin || !m_costume || !m_engine) return; - if (m_spriteModel) { - if (isVisible()) { - double stageWidth = m_engine->stageWidth(); - double stageHeight = m_engine->stageHeight(); - setX(m_stageScale * (stageWidth / 2 + m_x - m_costume->rotationCenterX() * m_clampedSize / m_costume->bitmapResolution() * (m_mirrorHorizontally ? -1 : 1))); - setY(m_stageScale * (stageHeight / 2 - m_y - m_costume->rotationCenterY() * m_clampedSize / m_costume->bitmapResolution())); - qreal originX = m_costume->rotationCenterX() * m_clampedSize * m_stageScale / m_costume->bitmapResolution(); - qreal originY = m_costume->rotationCenterY() * m_clampedSize * m_stageScale / m_costume->bitmapResolution(); - setTransformOriginPoint(QPointF(originX, originY)); - } - } else { + if (isVisible() || m_stageModel) { double stageWidth = m_engine->stageWidth(); double stageHeight = m_engine->stageHeight(); - setX(m_stageScale * (stageWidth / 2 - m_costume->rotationCenterX() / m_costume->bitmapResolution())); - setY(m_stageScale * (stageHeight / 2 - m_costume->rotationCenterY() / m_costume->bitmapResolution())); - qreal originX = m_costume->rotationCenterX() / m_costume->bitmapResolution(); - qreal originY = m_costume->rotationCenterY() / m_costume->bitmapResolution(); + setX(m_stageScale * (stageWidth / 2 + m_x - m_costume->rotationCenterX() * m_size / scale() / m_costume->bitmapResolution() * (m_mirrorHorizontally ? -1 : 1))); + setY(m_stageScale * (stageHeight / 2 - m_y - m_costume->rotationCenterY() * m_size / scale() / m_costume->bitmapResolution())); + qreal originX = m_costume->rotationCenterX() * m_stageScale * m_size / scale() / m_costume->bitmapResolution(); + qreal originY = m_costume->rotationCenterY() * m_stageScale * m_size / scale() / m_costume->bitmapResolution(); setTransformOriginPoint(QPointF(originX, originY)); + + // Qt ignores the transform origin point if it's (0, 0), + // so set the transform origin to top left in this case. + if (originX == 0 && originY == 0) + setTransformOrigin(QQuickItem::TopLeft); + else + setTransformOrigin(QQuickItem::Center); } } @@ -515,30 +508,13 @@ void RenderedTarget::calculateRotation() void RenderedTarget::calculateSize() { - if (m_costume) { - double bitmapRes = m_costume->bitmapResolution(); - - if (m_costumeWidth == 0 || m_costumeHeight == 0) - m_maxSize = 1; - else - m_maxSize = std::min(m_maximumWidth / (m_costumeWidth * m_stageScale), m_maximumHeight / (m_costumeHeight * m_stageScale)); - - if (m_spriteModel) { - m_clampedSize = std::min(m_size, m_maxSize); - m_width = m_costumeWidth * m_clampedSize * m_stageScale / bitmapRes; - m_height = m_height = m_costumeHeight * m_clampedSize * m_stageScale / bitmapRes; - } else { - m_width = m_costumeWidth * m_stageScale / bitmapRes; - m_height = m_height = m_costumeHeight * m_stageScale / bitmapRes; - } + 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()); } - - Q_ASSERT(m_maxSize > 0); - - if (!m_stageModel && (m_size > m_maxSize) && (m_maxSize != 0)) - setScale(m_size / m_maxSize); - else - setScale(1); } void RenderedTarget::handleSceneMouseMove(qreal x, qreal y) @@ -554,16 +530,6 @@ void RenderedTarget::handleSceneMouseMove(qreal x, qreal y) } } -QBuffer *RenderedTarget::bitmapBuffer() -{ - return &m_bitmapBuffer; -} - -const QString &RenderedTarget::bitmapUniqueKey() const -{ - return m_bitmapUniqueKey; -} - void RenderedTarget::lockCostume() { m_costumeMutex.lock(); @@ -578,11 +544,3 @@ bool RenderedTarget::mirrorHorizontally() const { return m_mirrorHorizontally; } - -bool RenderedTarget::isSvg() const -{ - if (!m_costume) - return false; - - return (m_costume->dataFormat() == "svg"); -} diff --git a/src/renderedtarget.h b/src/renderedtarget.h index 580278e..418b131 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -9,6 +9,7 @@ #include #include "irenderedtarget.h" +#include "texture.h" Q_MOC_INCLUDE("stagemodel.h"); Q_MOC_INCLUDE("spritemodel.h"); @@ -17,6 +18,8 @@ Q_MOC_INCLUDE("scenemousearea.h"); namespace scratchcpprender { +class Skin; + class RenderedTarget : public IRenderedTarget { Q_OBJECT @@ -30,6 +33,7 @@ class RenderedTarget : public IRenderedTarget public: RenderedTarget(QNanoQuickItem *parent = nullptr); + ~RenderedTarget(); void updateVisibility(bool visible) override; void updateX(double x) override; @@ -40,6 +44,9 @@ class RenderedTarget : public IRenderedTarget void updateLayerOrder(int layerOrder) override; void updateCostume(libscratchcpp::Costume *costume) override; + bool costumesLoaded() const override; + void loadCostumes() override; + void beforeRedraw() override; void deinitClone() override; @@ -69,16 +76,12 @@ class RenderedTarget : public IRenderedTarget QPointF mapFromScene(const QPointF &point) const override; - QBuffer *bitmapBuffer() override; - const QString &bitmapUniqueKey() const override; - void lockCostume() override; void unlockCostume() override; bool mirrorHorizontally() const override; - bool isSvg() const override; - void paintSvg(QNanoPainter *painter) override; + Texture texture() const override; void updateHullPoints(QOpenGLFramebufferObject *fbo) override; const std::vector &hullPoints() const override; @@ -110,21 +113,18 @@ class RenderedTarget : public IRenderedTarget StageModel *m_stageModel = nullptr; SpriteModel *m_spriteModel = nullptr; SceneMouseArea *m_mouseArea = nullptr; - QSvgRenderer m_svgRenderer; - QImage m_costumeBitmap; - QBuffer m_bitmapBuffer; - QString m_bitmapUniqueKey; + bool m_costumesLoaded = false; + std::unordered_map m_skins; + Skin *m_skin = nullptr; + Texture m_texture; + Texture m_oldTexture; QMutex m_costumeMutex; QMutex mutex; double m_size = 1; - double m_clampedSize = 1; - double m_maxSize = 1; - unsigned int m_costumeWidth = 0; - unsigned int m_costumeHeight = 0; double m_x = 0; double m_y = 0; - double m_width = 0; - double m_height = 0; + double m_width = 1; + double m_height = 1; double m_direction = 90; libscratchcpp::Sprite::RotationStyle m_rotationStyle = libscratchcpp::Sprite::RotationStyle::AllAround; bool m_mirrorHorizontally = false; diff --git a/src/targetpainter.cpp b/src/targetpainter.cpp index c91c3e9..0a75167 100644 --- a/src/targetpainter.cpp +++ b/src/targetpainter.cpp @@ -1,15 +1,18 @@ // SPDX-License-Identifier: LGPL-3.0-or-later +#include #include #include #include "targetpainter.h" #include "irenderedtarget.h" #include "spritemodel.h" +#include "bitmapskin.h" using namespace scratchcpprender; -TargetPainter::TargetPainter() +TargetPainter::TargetPainter(QOpenGLFramebufferObject *fbo) : + m_fbo(fbo) { } @@ -25,36 +28,51 @@ void TargetPainter::paint(QNanoPainter *painter) } m_target->lockCostume(); - double width = m_target->width(); - double height = m_target->height(); - if (m_target->isSvg()) - m_target->paintSvg(painter); - else { - QOpenGLContext *context = QOpenGLContext::currentContext(); - Q_ASSERT(context); + QOpenGLContext *context = QOpenGLContext::currentContext(); + Q_ASSERT(context); - if (!context) - return; + if (!context) + return; - QOffscreenSurface surface; - surface.setFormat(context->format()); - surface.create(); - Q_ASSERT(surface.isValid()); + // Custom FBO - only used for testing + QOpenGLFramebufferObject *targetFbo = m_fbo ? m_fbo : framebufferObject(); - QSurface *oldSurface = context->surface(); - context->makeCurrent(&surface); + QOpenGLExtraFunctions glF(context); + glF.initializeOpenGLFunctions(); - painter->beginFrame(width, height); - QNanoImage image = QNanoImage::fromCache(painter, m_target->bitmapBuffer(), m_target->bitmapUniqueKey()); - painter->drawImage(image, 0, 0, width, height); - painter->endFrame(); + // Cancel current frame because we're using a custom FBO + painter->cancelFrame(); - context->doneCurrent(); - context->makeCurrent(oldSurface); + Texture texture = m_target->texture(); + + if (!texture.isValid()) { + m_target->unlockCostume(); + return; + } + + // Create a FBO for the current texture + unsigned int fbo; + glF.glGenFramebuffers(1, &fbo); + glF.glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glF.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.handle(), 0); + + if (glF.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + m_target->unlockCostume(); + qWarning() << "error: framebuffer incomplete (" + m_target->scratchTarget()->name() + ")"; + glF.glDeleteFramebuffers(1, &fbo); + return; } - m_target->updateHullPoints(framebufferObject()); + // Blit the FBO to the Qt Quick FBO + glF.glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + glF.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, targetFbo->handle()); + glF.glBlitFramebuffer(0, 0, texture.width(), texture.height(), 0, 0, targetFbo->width(), targetFbo->height(), GL_COLOR_BUFFER_BIT, GL_NEAREST); + glF.glBindFramebuffer(GL_FRAMEBUFFER, targetFbo->handle()); + + glF.glDeleteFramebuffers(1, &fbo); + + m_target->updateHullPoints(targetFbo); m_target->unlockCostume(); } @@ -62,4 +80,10 @@ void TargetPainter::synchronize(QNanoQuickItem *item) { m_target = dynamic_cast(item); Q_ASSERT(m_target); + + // Render costumes into textures + if (!m_target->costumesLoaded()) { + m_target->loadCostumes(); + invalidateFramebufferObject(); + } } diff --git a/src/targetpainter.h b/src/targetpainter.h index e752f96..9c6f4b2 100644 --- a/src/targetpainter.h +++ b/src/targetpainter.h @@ -8,17 +8,19 @@ namespace scratchcpprender { class IRenderedTarget; +class Skin; class TargetPainter : public QNanoQuickItemPainter { public: - TargetPainter(); + TargetPainter(QOpenGLFramebufferObject *fbo = nullptr); ~TargetPainter(); void paint(QNanoPainter *painter) override; void synchronize(QNanoQuickItem *item) override; private: + QOpenGLFramebufferObject *m_fbo = nullptr; IRenderedTarget *m_target = nullptr; }; diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h index 019e70c..46b6109 100644 --- a/test/mocks/renderedtargetmock.h +++ b/test/mocks/renderedtargetmock.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -21,6 +22,9 @@ class RenderedTargetMock : public IRenderedTarget MOCK_METHOD(void, updateLayerOrder, (int), (override)); MOCK_METHOD(void, updateCostume, (libscratchcpp::Costume *), (override)); + MOCK_METHOD(bool, costumesLoaded, (), (const, override)); + MOCK_METHOD(void, loadCostumes, (), (override)); + MOCK_METHOD(void, beforeRedraw, (), (override)); MOCK_METHOD(void, deinitClone, (), (override)); @@ -50,16 +54,12 @@ class RenderedTargetMock : public IRenderedTarget MOCK_METHOD(QPointF, mapFromScene, (const QPointF &), (const, override)); - MOCK_METHOD(QBuffer *, bitmapBuffer, (), (override)); - MOCK_METHOD(const QString &, bitmapUniqueKey, (), (const, override)); - MOCK_METHOD(void, lockCostume, (), (override)); MOCK_METHOD(void, unlockCostume, (), (override)); MOCK_METHOD(bool, mirrorHorizontally, (), (const, override)); - MOCK_METHOD(bool, isSvg, (), (const, override)); - MOCK_METHOD(void, paintSvg, (QNanoPainter *), (override)); + MOCK_METHOD(Texture, texture, (), (const, override)); MOCK_METHOD(void, updateHullPoints, (QOpenGLFramebufferObject *), (override)); MOCK_METHOD(const std::vector &, hullPoints, (), (const, override)); diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index e541146..e059b5c 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -50,39 +50,53 @@ TEST_F(RenderedTargetTest, Constructors) TEST_F(RenderedTargetTest, UpdateMethods) { + QOpenGLContext context; + QOffscreenSurface surface; + createContextAndSurface(&context, &surface); RenderedTarget parent; // a parent item is needed for setVisible() to work RenderedTarget target(&parent); QSignalSpy mirrorHorizontallySpy(&target, &RenderedTarget::mirrorHorizontallyChanged); + ASSERT_FALSE(target.costumesLoaded()); // Stage Stage stage; StageModel stageModel; stage.setInterface(&stageModel); target.setStageModel(&stageModel); - Costume costume("", "", ""); + auto costume = std::make_shared("", "", "png"); std::string costumeData = readFileStr("image.png"); - costume.setData(costumeData.size(), static_cast(costumeData.data())); - costume.setRotationCenterX(-23); - costume.setRotationCenterY(72); - costume.setBitmapResolution(2.5); + costume->setData(costumeData.size(), static_cast(costumeData.data())); + costume->setRotationCenterX(-23); + costume->setRotationCenterY(72); + costume->setBitmapResolution(2.5); + 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)); - target.updateCostume(&costume); + target.updateCostume(costume.get()); target.beforeRedraw(); - ASSERT_EQ(target.width(), 1.6); - ASSERT_EQ(target.height(), 2.4); - ASSERT_EQ(target.x(), 249.2); - ASSERT_EQ(target.y(), 151.2); + ASSERT_EQ(target.width(), 4); + ASSERT_EQ(target.height(), 6); + ASSERT_EQ(target.x(), 263); + ASSERT_EQ(target.y(), 108); ASSERT_EQ(target.z(), 0); ASSERT_EQ(target.rotation(), 0); - ASSERT_EQ(target.transformOriginPoint(), QPointF(-9.2, 28.8)); + ASSERT_EQ(target.transformOriginPoint(), QPointF(-23, 72)); + ASSERT_EQ(target.transformOrigin(), QQuickItem::Center); + ASSERT_EQ(target.scale(), 0.4); target.setStageModel(nullptr); ASSERT_TRUE(mirrorHorizontallySpy.empty()); + Texture texture = target.texture(); + ASSERT_TRUE(texture.isValid()); + ASSERT_EQ(texture.width(), 4); + ASSERT_EQ(texture.height(), 6); + // Sprite Sprite sprite; sprite.setVisible(true); @@ -92,24 +106,33 @@ TEST_F(RenderedTargetTest, UpdateMethods) sprite.setX(0); sprite.setY(0); sprite.setLayerOrder(3); + sprite.addCostume(costume); SpriteModel spriteModel; sprite.setInterface(&spriteModel); - EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); - EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + EXPECT_CALL(engine, stageWidth()).Times(2).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).Times(2).WillRepeatedly(Return(360)); target.setSpriteModel(&spriteModel); + target.loadCostumes(); target.beforeRedraw(); - ASSERT_EQ(std::round(target.width() * 100) / 100, 2.3); - ASSERT_EQ(std::round(target.height() * 100) / 100, 3.46); - ASSERT_EQ(std::round(target.x() * 100) / 100, 253.25); - ASSERT_EQ(std::round(target.y() * 100) / 100, 138.53); + ASSERT_EQ(target.width(), 4); + ASSERT_EQ(target.height(), 6); + ASSERT_EQ(target.x(), 263); + ASSERT_EQ(target.y(), 108); ASSERT_EQ(target.z(), 3); ASSERT_EQ(target.rotation(), -157.16); - ASSERT_EQ(std::round(target.transformOriginPoint().x() * 100) / 100, -13.25); - ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 41.47); + ASSERT_EQ(target.transformOriginPoint().x(), -23); + ASSERT_EQ(target.transformOriginPoint().y(), 72); + ASSERT_EQ(target.transformOrigin(), QQuickItem::Center); + ASSERT_EQ(std::round(target.scale() * 100) / 100, 0.58); ASSERT_TRUE(mirrorHorizontallySpy.empty()); + texture = target.texture(); + ASSERT_TRUE(texture.isValid()); + ASSERT_EQ(texture.width(), 4); + ASSERT_EQ(texture.height(), 6); + // Visibility target.updateVisibility(false); ASSERT_FALSE(target.isVisible()); @@ -123,27 +146,27 @@ TEST_F(RenderedTargetTest, UpdateMethods) EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.updateX(12.5); - ASSERT_EQ(std::round(target.x() * 100) / 100, 265.75); - ASSERT_EQ(std::round(target.y() * 100) / 100, 138.53); + ASSERT_EQ(target.x(), 275.5); + 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(std::round(target.x() * 100) / 100, 265.75); - ASSERT_EQ(std::round(target.y() * 100) / 100, 214.62); + ASSERT_EQ(target.x(), 275.5); + ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09); // Size EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.updateSize(56.2); target.beforeRedraw(); - ASSERT_EQ(std::round(target.width() * 100) / 100, 0.9); - ASSERT_EQ(std::round(target.height() * 100) / 100, 1.35); - ASSERT_EQ(std::round(target.x() * 100) / 100, 257.67); - ASSERT_EQ(std::round(target.y() * 100) / 100, 239.9); - ASSERT_EQ(std::round(target.transformOriginPoint().x() * 100) / 100, -5.17); - ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 16.19); + 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.transformOriginPoint().x(), -23); + ASSERT_EQ(target.transformOriginPoint().y(), 72); // Direction target.updateDirection(123.8); @@ -156,8 +179,8 @@ TEST_F(RenderedTargetTest, UpdateMethods) target.updateRotationStyle(Sprite::RotationStyle::LeftRight); ASSERT_EQ(target.mirrorHorizontally(), false); ASSERT_EQ(target.rotation(), 0); - ASSERT_EQ(std::round(target.x() * 100) / 100, 257.67); - ASSERT_EQ(std::round(target.y() * 100) / 100, 239.9); + ASSERT_EQ(target.x(), 275.5); + ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09); ASSERT_TRUE(mirrorHorizontallySpy.empty()); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); @@ -165,8 +188,8 @@ TEST_F(RenderedTargetTest, UpdateMethods) target.updateDirection(-15); ASSERT_EQ(target.mirrorHorizontally(), true); ASSERT_EQ(target.rotation(), 0); - ASSERT_EQ(std::round(target.x() * 100) / 100, 247.33); - ASSERT_EQ(std::round(target.y() * 100) / 100, 239.9); + ASSERT_EQ(target.x(), 229.5); + ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09); ASSERT_EQ(mirrorHorizontallySpy.count(), 1); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); @@ -174,8 +197,8 @@ TEST_F(RenderedTargetTest, UpdateMethods) target.updateDirection(134.89); ASSERT_EQ(target.mirrorHorizontally(), false); ASSERT_EQ(target.rotation(), 0); - ASSERT_EQ(std::round(target.x() * 100) / 100, 257.67); - ASSERT_EQ(std::round(target.y() * 100) / 100, 239.9); + ASSERT_EQ(target.x(), 275.5); + ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09); ASSERT_EQ(mirrorHorizontallySpy.count(), 2); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); @@ -184,110 +207,69 @@ TEST_F(RenderedTargetTest, UpdateMethods) ASSERT_EQ(target.mirrorHorizontally(), false); ASSERT_EQ(target.rotation(), 0); ASSERT_EQ(mirrorHorizontallySpy.count(), 2); -} - -TEST_F(RenderedTargetTest, LoadJpegCostume) -{ - std::string str = readFileStr("image.jpg"); - Costume costume("", "abc", "jpg"); - costume.setData(str.size(), static_cast(const_cast(str.c_str()))); - costume.setBitmapResolution(3); - - RenderedTarget target; - target.updateCostume(&costume); - ASSERT_FALSE(target.isSvg()); - ASSERT_FALSE(target.bitmapBuffer()->isOpen()); - target.bitmapBuffer()->open(QBuffer::ReadOnly); - ASSERT_EQ(target.bitmapBuffer()->readAll().toStdString(), str); - ASSERT_EQ(target.bitmapUniqueKey().toStdString(), costume.id()); -} - -TEST_F(RenderedTargetTest, LoadPngCostume) -{ - std::string str = readFileStr("image.png"); - Costume costume("", "abc", "png"); - costume.setData(str.size(), static_cast(const_cast(str.c_str()))); - costume.setBitmapResolution(3); - - RenderedTarget target; - target.updateCostume(&costume); - ASSERT_FALSE(target.isSvg()); - ASSERT_FALSE(target.bitmapBuffer()->isOpen()); - target.bitmapBuffer()->open(QBuffer::ReadOnly); - ASSERT_EQ(target.bitmapBuffer()->readAll().toStdString(), str); - ASSERT_EQ(target.bitmapUniqueKey().toStdString(), costume.id()); -} - -TEST_F(RenderedTargetTest, LoadSvgCostume) -{ - // Get maximum viewport dimensions - QOpenGLContext context; - context.create(); - Q_ASSERT(context.isValid()); - - QOffscreenSurface surface; - surface.create(); - Q_ASSERT(surface.isValid()); - - context.makeCurrent(&surface); - GLint dims[2]; - glGetIntegerv(GL_MAX_VIEWPORT_DIMS, dims); - double maxWidth = dims[0] * 0.1; - double maxHeight = dims[1] * 0.1; - double maxSize = std::min(maxWidth / (1143 / 90.0), maxHeight / (1143 / 90.0)); - context.doneCurrent(); - - std::string str = readFileStr("image.svg"); - auto costume = std::make_shared("", "abc", "svg"); - costume->setData(str.size(), static_cast(const_cast(str.c_str()))); - costume->setBitmapResolution(1); - - EngineMock engine; - SpriteModel model; - Sprite sprite; - sprite.setSize(maxSize * 100); - sprite.setX(49.7); - sprite.setY(-64.15); - costume->setRotationCenterX(-84); - costume->setRotationCenterY(53); - model.init(&sprite); - - RenderedTarget target; - target.setEngine(&engine); + // Stage scale EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); - target.setSpriteModel(&model); - target.updateCostume(costume.get()); + target.setStageScale(1.5); target.beforeRedraw(); - ASSERT_TRUE(target.isSvg()); - ASSERT_FALSE(target.bitmapBuffer()->isOpen()); - target.bitmapBuffer()->open(QBuffer::ReadOnly); - ASSERT_TRUE(target.bitmapBuffer()->readAll().toStdString().empty()); - ASSERT_TRUE(target.bitmapUniqueKey().toStdString().empty()); - target.bitmapBuffer()->close(); - - ASSERT_EQ(std::round(target.width() * 100) / 100, 1548.09); - ASSERT_EQ(std::round(target.height() * 100) / 100, 1548.09); - ASSERT_EQ(target.scale(), 1); - ASSERT_EQ(std::round(target.x() * 100) / 100, 11126.36); - ASSERT_EQ(std::round(target.y() * 100) / 100, -6593.27); - ASSERT_EQ(std::round(target.transformOriginPoint().x() * 100) / 100, -10836.66); - ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 6837.42); - - // Test scale limit + 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.z(), 3); + ASSERT_EQ(target.rotation(), 0); + ASSERT_EQ(target.transformOriginPoint().x(), -23); + ASSERT_EQ(target.transformOriginPoint().y(), 72); + ASSERT_EQ(target.transformOrigin(), QQuickItem::Center); + ASSERT_EQ(std::round(target.scale() * 100) / 100, 0.34); + + // Null rotation center + costume->setRotationCenterX(0); + costume->setRotationCenterY(0); + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + target.updateSize(100); + 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.transformOriginPoint().x(), 0); + ASSERT_EQ(target.transformOriginPoint().y(), 0); + ASSERT_EQ(target.transformOrigin(), QQuickItem::TopLeft); + + // SVG + costume = std::make_shared("", "", "svg"); + std::string svgCostumeData = readFileStr("image.svg"); + costume->setData(svgCostumeData.size(), static_cast(svgCostumeData.data())); + costume->setRotationCenterX(25); + costume->setRotationCenterY(-8); + sprite.addCostume(costume); + EXPECT_CALL(engine, stageWidth()).Times(2).WillRepeatedly(Return(480)); EXPECT_CALL(engine, stageHeight()).Times(2).WillRepeatedly(Return(360)); - target.updateSize(maxSize * 250); - target.setStageScale(3.89); - - ASSERT_EQ(std::round(target.width() * 100) / 100, 1548.09); - ASSERT_EQ(std::round(target.height() * 100) / 100, 1548.09); - ASSERT_EQ(std::round(target.scale() * 100) / 100, 9.19); - ASSERT_EQ(std::round(target.x() * 100) / 100, 12595.73); - ASSERT_EQ(std::round(target.y() * 100) / 100, -6286.52); - ASSERT_EQ(std::round(target.transformOriginPoint().x() * 100) / 100, -11468.8); - ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 7236.27); + target.loadCostumes(); + target.updateCostume(costume.get()); + target.beforeRedraw(); + + 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.z(), 3); + ASSERT_EQ(target.rotation(), 0); + ASSERT_EQ(target.transformOriginPoint().x(), 50); + ASSERT_EQ(target.transformOriginPoint().y(), -16); + ASSERT_EQ(target.transformOrigin(), QQuickItem::Center); + ASSERT_EQ(std::round(target.scale() * 100) / 100, 0.75); + + texture = target.texture(); + ASSERT_TRUE(texture.isValid()); + ASSERT_EQ(texture.width(), 26); + ASSERT_EQ(texture.height(), 26); + + context.doneCurrent(); } TEST_F(RenderedTargetTest, DeinitClone) @@ -305,61 +287,6 @@ TEST_F(RenderedTargetTest, DeinitClone) ASSERT_EQ(mouseArea.draggedSprite(), nullptr); } -TEST_F(RenderedTargetTest, PaintSvg) -{ - std::string str = readFileStr("image.svg"); - Costume costume("", "abc", "svg"); - costume.setData(str.size(), static_cast(const_cast(str.c_str()))); - costume.setBitmapResolution(3); - - EngineMock engine; - Sprite sprite; - sprite.setSize(2525.7); - - SpriteModel model; - model.init(&sprite); - - EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); - EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); - RenderedTarget target; - target.setEngine(&engine); - target.setSpriteModel(&model); - target.updateCostume(&costume); - target.beforeRedraw(); - - // Create OpenGL context - QOpenGLContext context; - QOffscreenSurface surface; - createContextAndSurface(&context, &surface); - - // Create a painter - QNanoPainter painter; - - QOpenGLFramebufferObjectFormat format; - format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); - - // Begin painting - QOpenGLFramebufferObject fbo(100, 100, format); - fbo.bind(); - painter.beginFrame(fbo.width(), fbo.height()); - - // Paint - target.paintSvg(&painter); - painter.endFrame(); - - // Compare with reference image - QBuffer buffer; - fbo.toImage().save(&buffer, "png"); - QFile ref("svg_result.png"); - ref.open(QFile::ReadOnly); - buffer.open(QBuffer::ReadOnly); - ASSERT_EQ(buffer.readAll(), ref.readAll()); - - // Release - fbo.release(); - context.doneCurrent(); -} - TEST_F(RenderedTargetTest, HullPoints) { EngineMock engine; diff --git a/test/svg_result.png b/test/svg_result.png deleted file mode 100644 index 86dceff10b5fba97df5cda66951d8429d7fb04ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 436 zcmeAS@N?(olHy`uVBq!ia0vp^DImd}{8@1NUh7ATV$?l)6bze=a0n?A$pD(faKLxl-Q*qHw|}3$`uIEl ze(QTD)2?xUifzdj>)Px$A=)V>O5u!@>Z=SU`Vxo5Kf?3M^N@iIRVe{4om z;4H^)K*>cBPXsQ{o?yE?naj76voq88gy3aE*JVdqj+Cf9Iq`A^Q*e?~5=*C`ira+Z z#_tiz9uL}AWUxG(jN;%QJ1Wi{ch%zrdcfzotWn$kf_Bc6f-1|to{&1cYt7>PhV@m# zM#uKY-M-BOq+YmcOv*5l files = { { "image.jpg", "jpeg_result.png" }, { "image.png", "png_result.png" } }; - - for (const auto &[inFile, outFile] : files) { - // Create target painter - TargetPainter targetPainter; - QNanoPainter painter; - RenderedTargetMock target; - targetPainter.synchronize(&target); - - // Load the image - QBuffer buffer; - buffer.open(QBuffer::WriteOnly); - std::string str = readFileStr(inFile); - buffer.write(str.c_str(), str.size()); - buffer.close(); - - QOpenGLFramebufferObjectFormat format; - format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); - - // Begin painting reference - QNanoPainter refPainter; - QOpenGLFramebufferObject refFbo(40, 60, format); - refFbo.bind(); - refPainter.beginFrame(refFbo.width(), refFbo.height()); - - // Paint reference - QNanoImage refImage = QNanoImage::fromCache(&refPainter, &buffer, "abc"); - refPainter.drawImage(refImage, 0, 0, 40, 60); - refPainter.endFrame(); - - // Begin painting - QOpenGLFramebufferObject fbo(40, 60, format); - fbo.bind(); - painter.beginFrame(fbo.width(), fbo.height()); - - // Paint - EXPECT_CALL(target, lockCostume()); - EXPECT_CALL(target, width()).WillOnce(Return(40)); - EXPECT_CALL(target, height()).WillOnce(Return(60)); - EXPECT_CALL(target, isSvg()).WillOnce(Return(false)); - EXPECT_CALL(target, bitmapBuffer()).WillOnce(Return(&buffer)); - static const QString uniqueKey("abc"); - EXPECT_CALL(target, bitmapUniqueKey()).WillOnce(ReturnRef(uniqueKey)); - EXPECT_CALL(target, updateHullPoints); - EXPECT_CALL(target, unlockCostume()); - targetPainter.paint(&painter); - painter.endFrame(); - - // Compare resulting images - ASSERT_EQ(fbo.toImage(), refFbo.toImage()); - - // Release - fbo.release(); - refFbo.release(); - } + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); - context.doneCurrent(); -} + // Begin painting reference + QNanoPainter refPainter; + QOpenGLFramebufferObject refFbo(40, 60, format); + refFbo.bind(); + refPainter.beginFrame(refFbo.width(), refFbo.height()); -TEST_F(TargetPainterTest, PaintSvg) -{ - QOpenGLContext context; - QOffscreenSurface surface; - createContextAndSurface(&context, &surface); + // Paint reference + refPainter.setAntialias(0); + refPainter.setStrokeStyle(QNanoColor(255, 0, 0, 128)); + refPainter.ellipse(refFbo.width() / 2, refFbo.height() / 2, refFbo.width() / 2, refFbo.height() / 2); + refPainter.stroke(); + refPainter.endFrame(); - TargetPainter targetPainter; + // Begin painting QNanoPainter painter; + QOpenGLFramebufferObject fbo(40, 60, format); + fbo.bind(); + painter.beginFrame(fbo.width(), fbo.height()); + + // Create target painter + TargetPainter targetPainter(&fbo); RenderedTargetMock target; + + EXPECT_CALL(target, costumesLoaded()).WillOnce(Return(false)); + EXPECT_CALL(target, loadCostumes()); + targetPainter.synchronize(&target); + + EXPECT_CALL(target, costumesLoaded()).WillOnce(Return(true)); + EXPECT_CALL(target, loadCostumes()).Times(0); targetPainter.synchronize(&target); + EXPECT_CALL(target, costumesLoaded()).WillOnce(Return(false)); + EXPECT_CALL(target, loadCostumes()); + targetPainter.synchronize(&target); + + // Paint + Texture texture(refFbo.texture(), refFbo.size()); EXPECT_CALL(target, lockCostume()); - EXPECT_CALL(target, width()).WillOnce(Return(40)); - EXPECT_CALL(target, height()).WillOnce(Return(60)); - EXPECT_CALL(target, isSvg()).WillOnce(Return(true)); - EXPECT_CALL(target, paintSvg(&painter)); - EXPECT_CALL(target, updateHullPoints); + EXPECT_CALL(target, texture()).WillOnce(Return(texture)); + EXPECT_CALL(target, updateHullPoints(&fbo)); EXPECT_CALL(target, unlockCostume()); targetPainter.paint(&painter); + painter.endFrame(); + + // Compare resulting images + ASSERT_EQ(fbo.toImage(), refFbo.toImage()); + + // Release + fbo.release(); + refFbo.release(); context.doneCurrent(); } From 7b379b442535f0917dc465af130c93bc75ef023b Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 20 Jan 2024 22:10:37 +0100 Subject: [PATCH 08/13] Remove obsolete mutexes --- src/irenderedtarget.h | 3 --- src/renderedtarget.cpp | 13 ------------- src/renderedtarget.h | 5 ----- src/targetpainter.cpp | 8 +------- test/mocks/renderedtargetmock.h | 3 --- test/targetpainter/targetpainter_test.cpp | 2 -- 6 files changed, 1 insertion(+), 33 deletions(-) diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h index 5412a7c..9850912 100644 --- a/src/irenderedtarget.h +++ b/src/irenderedtarget.h @@ -69,9 +69,6 @@ class IRenderedTarget : public QNanoQuickItem virtual QPointF mapFromScene(const QPointF &point) const = 0; - virtual void lockCostume() = 0; - virtual void unlockCostume() = 0; - virtual bool mirrorHorizontally() const = 0; virtual Texture texture() const = 0; diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index defb114..abfbee4 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -98,7 +98,6 @@ void RenderedTarget::updateCostume(Costume *costume) if (!costume || costume == m_costume) return; - m_costumeMutex.lock(); m_costume = costume; if (m_costumesLoaded) { @@ -110,8 +109,6 @@ void RenderedTarget::updateCostume(Costume *costume) m_skin = it->second; } - m_costumeMutex.unlock(); - calculateSize(); calculatePos(); } @@ -530,16 +527,6 @@ void RenderedTarget::handleSceneMouseMove(qreal x, qreal y) } } -void RenderedTarget::lockCostume() -{ - m_costumeMutex.lock(); -} - -void RenderedTarget::unlockCostume() -{ - m_costumeMutex.unlock(); -} - bool RenderedTarget::mirrorHorizontally() const { return m_mirrorHorizontally; diff --git a/src/renderedtarget.h b/src/renderedtarget.h index 418b131..9f84b8d 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -76,9 +76,6 @@ class RenderedTarget : public IRenderedTarget QPointF mapFromScene(const QPointF &point) const override; - void lockCostume() override; - void unlockCostume() override; - bool mirrorHorizontally() const override; Texture texture() const override; @@ -118,8 +115,6 @@ class RenderedTarget : public IRenderedTarget Skin *m_skin = nullptr; Texture m_texture; Texture m_oldTexture; - QMutex m_costumeMutex; - QMutex mutex; double m_size = 1; double m_x = 0; double m_y = 0; diff --git a/src/targetpainter.cpp b/src/targetpainter.cpp index 0a75167..7d64645 100644 --- a/src/targetpainter.cpp +++ b/src/targetpainter.cpp @@ -27,8 +27,6 @@ void TargetPainter::paint(QNanoPainter *painter) "application object."); } - m_target->lockCostume(); - QOpenGLContext *context = QOpenGLContext::currentContext(); Q_ASSERT(context); @@ -46,10 +44,8 @@ void TargetPainter::paint(QNanoPainter *painter) Texture texture = m_target->texture(); - if (!texture.isValid()) { - m_target->unlockCostume(); + if (!texture.isValid()) return; - } // Create a FBO for the current texture unsigned int fbo; @@ -58,7 +54,6 @@ void TargetPainter::paint(QNanoPainter *painter) glF.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.handle(), 0); if (glF.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - m_target->unlockCostume(); qWarning() << "error: framebuffer incomplete (" + m_target->scratchTarget()->name() + ")"; glF.glDeleteFramebuffers(1, &fbo); return; @@ -73,7 +68,6 @@ void TargetPainter::paint(QNanoPainter *painter) glF.glDeleteFramebuffers(1, &fbo); m_target->updateHullPoints(targetFbo); - m_target->unlockCostume(); } void TargetPainter::synchronize(QNanoQuickItem *item) diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h index 46b6109..b7ddf7f 100644 --- a/test/mocks/renderedtargetmock.h +++ b/test/mocks/renderedtargetmock.h @@ -54,9 +54,6 @@ class RenderedTargetMock : public IRenderedTarget MOCK_METHOD(QPointF, mapFromScene, (const QPointF &), (const, override)); - MOCK_METHOD(void, lockCostume, (), (override)); - MOCK_METHOD(void, unlockCostume, (), (override)); - MOCK_METHOD(bool, mirrorHorizontally, (), (const, override)); MOCK_METHOD(Texture, texture, (), (const, override)); diff --git a/test/targetpainter/targetpainter_test.cpp b/test/targetpainter/targetpainter_test.cpp index 18a20a3..a33b42a 100644 --- a/test/targetpainter/targetpainter_test.cpp +++ b/test/targetpainter/targetpainter_test.cpp @@ -76,10 +76,8 @@ TEST_F(TargetPainterTest, Paint) // Paint Texture texture(refFbo.texture(), refFbo.size()); - EXPECT_CALL(target, lockCostume()); EXPECT_CALL(target, texture()).WillOnce(Return(texture)); EXPECT_CALL(target, updateHullPoints(&fbo)); - EXPECT_CALL(target, unlockCostume()); targetPainter.paint(&painter); painter.endFrame(); From af14d11b8099193c145cb5ac9c62f266d68c116e Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 20 Jan 2024 22:32:12 +0100 Subject: [PATCH 09/13] Add cloneRoot() getter to SpriteModel --- src/spritemodel.cpp | 8 ++++++++ src/spritemodel.h | 2 ++ test/target_models/spritemodel_test.cpp | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp index 66e9d41..e6e2c1d 100644 --- a/src/spritemodel.cpp +++ b/src/spritemodel.cpp @@ -115,4 +115,12 @@ void SpriteModel::setRenderedTarget(IRenderedTarget *newRenderedTarget) emit renderedTargetChanged(); } +SpriteModel *SpriteModel::cloneRoot() const +{ + if (m_cloneRoot == this) + return nullptr; + else + return m_cloneRoot; +} + } // namespace scratchcpprender diff --git a/src/spritemodel.h b/src/spritemodel.h index b7bb5b7..8be730b 100644 --- a/src/spritemodel.h +++ b/src/spritemodel.h @@ -49,6 +49,8 @@ class SpriteModel IRenderedTarget *renderedTarget() const; void setRenderedTarget(IRenderedTarget *newRenderedTarget); + SpriteModel *cloneRoot() const; + signals: void renderedTargetChanged(); void cloned(SpriteModel *cloneModel); diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp index 6c2ca19..fc6a136 100644 --- a/test/target_models/spritemodel_test.cpp +++ b/test/target_models/spritemodel_test.cpp @@ -40,11 +40,13 @@ TEST(SpriteModelTest, DeInitClone) TEST(SpriteModelTest, OnCloned) { SpriteModel model; + ASSERT_EQ(model.cloneRoot(), nullptr); Sprite clone1; QSignalSpy spy1(&model, &SpriteModel::cloned); model.onCloned(&clone1); ASSERT_EQ(spy1.count(), 1); + ASSERT_EQ(model.cloneRoot(), nullptr); QList args = spy1.takeFirst(); ASSERT_EQ(args.size(), 1); @@ -52,6 +54,7 @@ TEST(SpriteModelTest, OnCloned) ASSERT_TRUE(cloneModel); ASSERT_EQ(cloneModel->parent(), &model); ASSERT_EQ(cloneModel->sprite(), &clone1); + ASSERT_EQ(cloneModel->cloneRoot(), &model); spy1.clear(); Sprite clone2; @@ -64,6 +67,7 @@ TEST(SpriteModelTest, OnCloned) ASSERT_TRUE(cloneModel); ASSERT_EQ(cloneModel->parent(), &model); ASSERT_EQ(cloneModel->sprite(), &clone2); + ASSERT_EQ(cloneModel->cloneRoot(), &model); Sprite clone3; QSignalSpy spy2(cloneModel, &SpriteModel::cloned); @@ -76,6 +80,7 @@ TEST(SpriteModelTest, OnCloned) ASSERT_TRUE(cloneModel); ASSERT_EQ(cloneModel->parent(), &model); ASSERT_EQ(cloneModel->sprite(), &clone3); + ASSERT_EQ(cloneModel->cloneRoot(), &model); } TEST(SpriteModelTest, OnCostumeChanged) From b53e5cd9e397d7a9f99e76d4b210b06ed24caa13 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 20 Jan 2024 22:32:50 +0100 Subject: [PATCH 10/13] RenderedTarget: Inherit skins from clone root --- src/renderedtarget.cpp | 27 +++++++++++++++++++++++---- src/renderedtarget.h | 1 + 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index abfbee4..ba7d6e6 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -25,8 +25,10 @@ RenderedTarget::RenderedTarget(QNanoQuickItem *parent) : RenderedTarget::~RenderedTarget() { - for (const auto &[costume, skin] : m_skins) - delete skin; + if (!m_skinsInherited) { + for (const auto &[costume, skin] : m_skins) + delete skin; + } } void RenderedTarget::updateVisibility(bool visible) @@ -121,9 +123,12 @@ bool RenderedTarget::costumesLoaded() const void RenderedTarget::loadCostumes() { // Delete previous skins - for (const auto &[costume, skin] : m_skins) - delete skin; + if (!m_skinsInherited) { + for (const auto &[costume, skin] : m_skins) + delete skin; + } + m_skinsInherited = false; m_skins.clear(); // Generate a skin for each costume @@ -227,6 +232,20 @@ void RenderedTarget::setSpriteModel(SpriteModel *newSpriteModel) m_spriteModel = newSpriteModel; if (m_spriteModel) { + SpriteModel *cloneRoot = m_spriteModel->cloneRoot(); + + if (cloneRoot) { + // Inherit skins from the clone root + RenderedTarget *target = dynamic_cast(cloneRoot->renderedTarget()); + Q_ASSERT(target); + + if (target->costumesLoaded()) { + m_skins = target->m_skins; // TODO: Avoid copying - maybe using a pointer? + m_costumesLoaded = true; + m_skinsInherited = true; // avoid double free + } + } + Sprite *sprite = m_spriteModel->sprite(); if (sprite) { diff --git a/src/renderedtarget.h b/src/renderedtarget.h index 9f84b8d..e486357 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -112,6 +112,7 @@ class RenderedTarget : public IRenderedTarget SceneMouseArea *m_mouseArea = nullptr; bool m_costumesLoaded = false; std::unordered_map m_skins; + bool m_skinsInherited = false; Skin *m_skin = nullptr; Texture m_texture; Texture m_oldTexture; From c56e995d5a4bd04c8c5b119a52e23820bcf138cf Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sat, 20 Jan 2024 22:40:13 +0100 Subject: [PATCH 11/13] Read pixels directly in RenderedTarget::updateHullPoints() --- src/renderedtarget.cpp | 9 --------- test/renderedtarget/renderedtarget_test.cpp | 13 ------------- 2 files changed, 22 deletions(-) diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index ba7d6e6..70f56fb 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -397,19 +397,10 @@ void RenderedTarget::updateHullPoints(QOpenGLFramebufferObject *fbo) m_hullPoints.clear(); m_hullPoints.reserve(width * height); - // Blit multisampled FBO to a custom FBO - QOpenGLFramebufferObject customFbo(fbo->size()); - glBindFramebuffer(GL_READ_FRAMEBUFFER_EXT, fbo->handle()); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER_EXT, customFbo.handle()); - glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST); - glBindFramebuffer(GL_FRAMEBUFFER_EXT, customFbo.handle()); - // 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); - glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0); - fbo->bind(); // Flip vertically int rowSize = width * 4; diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index e059b5c..576a1de 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -325,19 +325,6 @@ TEST_F(RenderedTargetTest, HullPoints) target.updateHullPoints(&fbo); ASSERT_EQ(target.hullPoints(), std::vector({ { 1, 1 }, { 2, 1 }, { 3, 1 }, { 1, 2 }, { 3, 2 }, { 1, 3 }, { 2, 3 }, { 3, 3 } })); - // Begin painting (multisampled) - format.setSamples(16); - QOpenGLFramebufferObject fboMultiSampled(4, 6, format); - fboMultiSampled.bind(); - painter.beginFrame(fboMultiSampled.width(), fboMultiSampled.height()); - - // Paint (multisampled) - painter.drawImage(image, 0, 0); - painter.endFrame(); - - // Test hull points (this is undefined with multisampling, so we just check if there are any hull points) - ASSERT_FALSE(target.hullPoints().empty()); - // Release fbo.release(); context.doneCurrent(); From a6314edf4c27a5d423f9be0ae95b4265c1ab6171 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 21 Jan 2024 00:18:28 +0100 Subject: [PATCH 12/13] Add getBounds() method to IRenderedTarget --- src/irenderedtarget.h | 2 + src/renderedtarget.cpp | 48 ++++++++++++++ src/renderedtarget.h | 3 + test/mocks/renderedtargetmock.h | 3 + test/renderedtarget/renderedtarget_test.cpp | 73 +++++++++++++++++++++ 5 files changed, 129 insertions(+) diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h index 9850912..2a2d1b9 100644 --- a/src/irenderedtarget.h +++ b/src/irenderedtarget.h @@ -67,6 +67,8 @@ class IRenderedTarget : public QNanoQuickItem virtual qreal height() const = 0; virtual void setHeight(qreal width) = 0; + virtual libscratchcpp::Rect getBounds() const = 0; + virtual QPointF mapFromScene(const QPointF &point) const = 0; virtual bool mirrorHorizontally() const = 0; diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 70f56fb..96892e8 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -17,6 +18,7 @@ using namespace scratchcpprender; 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) : IRenderedTarget(parent) @@ -327,6 +329,43 @@ void RenderedTarget::setHeight(qreal height) QNanoQuickItem::setHeight(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()) + 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 rot = -rotation() * pi / 180; + double left = std::numeric_limits::infinity(); + double top = -std::numeric_limits::infinity(); + double right = -std::numeric_limits::infinity(); + double bottom = std::numeric_limits::infinity(); + + for (const QPointF &point : m_hullPoints) { + 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; + + if (x < left) + left = x; + + if (x > right) + right = x; + + if (y > top) + top = y; + + if (y < bottom) + bottom = y; + } + + return Rect(left + m_x, top + m_y, right + m_x, bottom + m_y); +} + QPointF RenderedTarget::mapFromScene(const QPointF &point) const { return QNanoQuickItem::mapFromScene(point); @@ -537,6 +576,15 @@ void RenderedTarget::handleSceneMouseMove(qreal x, qreal y) } } +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); + const double x = (scratchX - originX) * cosRot - (scratchY - originY) * sinRot; + const double y = (scratchX - originX) * sinRot + (scratchY - originY) * cosRot; + return QPointF(x, y); +} + bool RenderedTarget::mirrorHorizontally() const { return m_mirrorHorizontally; diff --git a/src/renderedtarget.h b/src/renderedtarget.h index e486357..2e0f5dc 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -74,6 +74,8 @@ class RenderedTarget : public IRenderedTarget qreal height() const override; void setHeight(qreal height) override; + libscratchcpp::Rect getBounds() const override; + QPointF mapFromScene(const QPointF &point) const override; bool mirrorHorizontally() const override; @@ -104,6 +106,7 @@ class RenderedTarget : public IRenderedTarget void calculateRotation(); void calculateSize(); void handleSceneMouseMove(qreal x, qreal y); + QPointF transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const; libscratchcpp::IEngine *m_engine = nullptr; libscratchcpp::Costume *m_costume = nullptr; diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h index b7ddf7f..ba4435d 100644 --- a/test/mocks/renderedtargetmock.h +++ b/test/mocks/renderedtargetmock.h @@ -3,6 +3,7 @@ #include #include #include +#include #include using namespace scratchcpprender; @@ -54,6 +55,8 @@ class RenderedTargetMock : public IRenderedTarget MOCK_METHOD(QPointF, mapFromScene, (const QPointF &), (const, override)); + MOCK_METHOD(libscratchcpp::Rect, getBounds, (), (const, override)); + MOCK_METHOD(bool, mirrorHorizontally, (), (const, override)); MOCK_METHOD(Texture, texture, (), (const, override)); diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index 576a1de..bba48ed 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "../common.h" @@ -554,3 +555,75 @@ TEST_F(RenderedTargetTest, StageScale) target.setStageScale(6.4); ASSERT_EQ(target.stageScale(), 6.4); } + +TEST_F(RenderedTargetTest, GetBounds) +{ + 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("", "", "png"); + std::string costumeData = readFileStr("image.png"); + costume->setData(costumeData.size(), static_cast(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(); + + 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); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, 66.72); + ASSERT_EQ(std::round(bounds.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, 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); + + 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); + + context.doneCurrent(); +} From ee71b83c0f823f4328f3b2a9b9a3f32fcb853d65 Mon Sep 17 00:00:00 2001 From: adazem009 <68537469+adazem009@users.noreply.github.com> Date: Sun, 21 Jan 2024 00:23:00 +0100 Subject: [PATCH 13/13] Implement SpriteModel::boundingRect() --- src/spritemodel.cpp | 2 +- test/target_models/spritemodel_test.cpp | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp index e6e2c1d..9064dda 100644 --- a/src/spritemodel.cpp +++ b/src/spritemodel.cpp @@ -93,7 +93,7 @@ void SpriteModel::onGraphicsEffectsCleared() libscratchcpp::Rect SpriteModel::boundingRect() const { - return libscratchcpp::Rect(m_sprite->x(), m_sprite->y(), m_sprite->x(), m_sprite->y()); + return m_renderedTarget->getBounds(); } libscratchcpp::Sprite *SpriteModel::sprite() const diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp index fc6a136..f5527d1 100644 --- a/test/target_models/spritemodel_test.cpp +++ b/test/target_models/spritemodel_test.cpp @@ -8,6 +8,8 @@ using namespace scratchcpprender; using namespace libscratchcpp; +using ::testing::Return; + TEST(SpriteModelTest, Constructors) { SpriteModel model1; @@ -175,6 +177,22 @@ TEST(SpriteModelTest, OnLayerOrderChanged) model.onLayerOrderChanged(7); } +TEST(SpriteModelTest, BoundingRect) +{ + SpriteModel model; + + RenderedTargetMock renderedTarget; + model.setRenderedTarget(&renderedTarget); + + Rect rect(-1, 1, 1, -1); + EXPECT_CALL(renderedTarget, getBounds()).WillOnce(Return(rect)); + Rect bounds = model.boundingRect(); + 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;