diff --git a/libscratchcpp b/libscratchcpp index d7e05e2..1964cce 160000 --- a/libscratchcpp +++ b/libscratchcpp @@ -1 +1 @@ -Subproject commit d7e05e2c01f15ecfad78e8a427b7cea9ce4253ec +Subproject commit 1964cce49460f2d6a1a9102e280362d225ac46df diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bb1993a..d2b4c85 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -49,6 +49,12 @@ qt_add_qml_module(scratchcpp-render mouseeventhandler.h keyeventhandler.cpp keyeventhandler.h + ipenlayer.h + penlayer.cpp + penlayer.h + penlayerpainter.cpp + penlayerpainter.h + penattributes.h ) list(APPEND QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml index 908dbf6..0627a42 100644 --- a/src/ProjectPlayer.qml +++ b/src/ProjectPlayer.qml @@ -109,6 +109,12 @@ ProjectScene { onStageModelChanged: stageModel.renderedTarget = this } + PenLayer { + id: projectPenLayer + engine: loader.engine + anchors.fill: parent + } + Component { id: renderedSprite @@ -121,6 +127,7 @@ ProjectScene { engine = loader.engine; spriteModel = modelData; spriteModel.renderedTarget = this; + spriteModel.penLayer = projectPenLayer; } } } diff --git a/src/ipenlayer.h b/src/ipenlayer.h new file mode 100644 index 0000000..54377e6 --- /dev/null +++ b/src/ipenlayer.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +namespace libscratchcpp +{ + +class IEngine; + +} + +namespace scratchcpprender +{ + +struct PenAttributes; + +class IPenLayer : public QNanoQuickItem +{ + public: + IPenLayer(QNanoQuickItem *parent = nullptr) : + QNanoQuickItem(parent) + { + } + + virtual ~IPenLayer() { } + + virtual bool antialiasingEnabled() const = 0; + virtual void setAntialiasingEnabled(bool enabled) = 0; + + virtual libscratchcpp::IEngine *engine() const = 0; + virtual void setEngine(libscratchcpp::IEngine *newEngine) = 0; + + virtual void clear() = 0; + virtual void drawPoint(const PenAttributes &penAttributes, double x, double y) = 0; + virtual void drawLine(const PenAttributes &penAttributes, double x0, double y0, double x1, double y1) = 0; + + virtual QOpenGLFramebufferObject *framebufferObject() const = 0; +}; + +} // namespace scratchcpprender diff --git a/src/penattributes.h b/src/penattributes.h new file mode 100644 index 0000000..900991f --- /dev/null +++ b/src/penattributes.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +namespace scratchcpprender +{ + +struct PenAttributes +{ + QColor color = QColor(0, 0, 255); + double diameter = 1; +}; + +} // namespace scratchcpprender diff --git a/src/penlayer.cpp b/src/penlayer.cpp new file mode 100644 index 0000000..bf85be4 --- /dev/null +++ b/src/penlayer.cpp @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "penlayer.h" +#include "penlayerpainter.h" +#include "penattributes.h" + +using namespace scratchcpprender; + +std::unordered_map PenLayer::m_projectPenLayers; + +PenLayer::PenLayer(QNanoQuickItem *parent) : + IPenLayer(parent) +{ + m_fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + m_fboFormat.setSamples(m_antialiasingEnabled ? 4 : 0); +} + +PenLayer::~PenLayer() +{ + if (m_engine) + m_projectPenLayers.erase(m_engine); +} + +bool PenLayer::antialiasingEnabled() const +{ + return m_antialiasingEnabled; +} + +void PenLayer::setAntialiasingEnabled(bool enabled) +{ + m_antialiasingEnabled = enabled; + m_fboFormat.setSamples(enabled ? 4 : 0); +} + +libscratchcpp::IEngine *PenLayer::engine() const +{ + return m_engine; +} + +void PenLayer::setEngine(libscratchcpp::IEngine *newEngine) +{ + if (m_engine == newEngine) + return; + + if (m_engine) + m_projectPenLayers.erase(m_engine); + + m_engine = newEngine; + + if (m_engine) { + m_projectPenLayers[m_engine] = this; + m_fbo = std::make_unique(m_engine->stageWidth(), m_engine->stageHeight(), m_fboFormat); + Q_ASSERT(m_fbo->isValid()); + + m_paintDevice = std::make_unique(m_fbo->size()); + clear(); + } + + emit engineChanged(); +} + +void scratchcpprender::PenLayer::clear() +{ + if (!m_fbo) + return; + + m_fbo->bind(); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + m_fbo->release(); + + update(); +} + +void scratchcpprender::PenLayer::drawPoint(const PenAttributes &penAttributes, double x, double y) +{ + drawLine(penAttributes, x, y, x, y); +} + +void scratchcpprender::PenLayer::drawLine(const PenAttributes &penAttributes, double x0, double y0, double x1, double y1) +{ + if (!m_fbo || !m_paintDevice || !m_engine) + return; + + // Begin painting + m_fbo->bind(); + QPainter painter(m_paintDevice.get()); + painter.beginNativePainting(); + painter.setRenderHint(QPainter::Antialiasing, m_antialiasingEnabled); + painter.setRenderHint(QPainter::SmoothPixmapTransform, false); + + // Translate to Scratch coordinate system + double stageWidthHalf = m_engine->stageWidth() / 2; + double stageHeightHalf = m_engine->stageHeight() / 2; + x0 += stageWidthHalf; + y0 = stageHeightHalf - y0; + x1 += stageWidthHalf; + y1 = stageHeightHalf - y1; + + // Set pen attributes + QPen pen(penAttributes.color); + pen.setWidthF(penAttributes.diameter); + pen.setCapStyle(Qt::RoundCap); + painter.setPen(pen); + + // If the start and end coordinates are the same, draw a point, otherwise draw a line + if (x0 == x1 && y0 == y1) + painter.drawPoint(x0, y0); + else + painter.drawLine(x0, y0, x1, y1); + + // End painting + painter.endNativePainting(); + painter.end(); + m_fbo->release(); + + update(); +} + +QOpenGLFramebufferObject *PenLayer::framebufferObject() const +{ + return m_fbo.get(); +} + +IPenLayer *PenLayer::getProjectPenLayer(libscratchcpp::IEngine *engine) +{ + auto it = m_projectPenLayers.find(engine); + + if (it != m_projectPenLayers.cend()) + return it->second; + + return nullptr; +} + +QNanoQuickItemPainter *PenLayer::createItemPainter() const +{ + return new PenLayerPainter; +} diff --git a/src/penlayer.h b/src/penlayer.h new file mode 100644 index 0000000..fc88ae2 --- /dev/null +++ b/src/penlayer.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +namespace scratchcpprender +{ + +class PenLayer : public IPenLayer +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(libscratchcpp::IEngine *engine READ engine WRITE setEngine NOTIFY engineChanged) + + public: + PenLayer(QNanoQuickItem *parent = nullptr); + ~PenLayer(); + + bool antialiasingEnabled() const override; + void setAntialiasingEnabled(bool enabled) override; + + libscratchcpp::IEngine *engine() const override; + void setEngine(libscratchcpp::IEngine *newEngine) override; + + void clear() override; + void drawPoint(const PenAttributes &penAttributes, double x, double y) override; + void drawLine(const PenAttributes &penAttributes, double x0, double y0, double x1, double y1) override; + + QOpenGLFramebufferObject *framebufferObject() const override; + + static IPenLayer *getProjectPenLayer(libscratchcpp::IEngine *engine); + + signals: + void engineChanged(); + + protected: + QNanoQuickItemPainter *createItemPainter() const override; + + private: + static std::unordered_map m_projectPenLayers; + bool m_antialiasingEnabled = true; + libscratchcpp::IEngine *m_engine = nullptr; + std::unique_ptr m_fbo; + std::unique_ptr m_paintDevice; + QOpenGLFramebufferObjectFormat m_fboFormat; +}; + +} // namespace scratchcpprender diff --git a/src/penlayerpainter.cpp b/src/penlayerpainter.cpp new file mode 100644 index 0000000..1ef6ea0 --- /dev/null +++ b/src/penlayerpainter.cpp @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "penlayerpainter.h" +#include "penlayer.h" + +using namespace scratchcpprender; + +PenLayerPainter::PenLayerPainter(QOpenGLFramebufferObject *fbo) +{ + m_targetFbo = fbo; +} + +void PenLayerPainter::paint(QNanoPainter *painter) +{ + if (QThread::currentThread() != qApp->thread()) { + qFatal("Error: Rendering must happen in the GUI thread to work correctly. Please disable threaded render loop using qputenv(\"QSG_RENDER_LOOP\", \"basic\") before constructing your " + "application object."); + } + + QOpenGLContext *context = QOpenGLContext::currentContext(); + Q_ASSERT(context); + + if (!context || !m_fbo) + return; + + // Custom FBO - only used for testing + QOpenGLFramebufferObject *targetFbo = m_targetFbo ? m_targetFbo : framebufferObject(); + + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + + // Blit the FBO to a temporary FBO first (multisampled FBOs can only be blitted to FBOs with the same size) + QOpenGLFramebufferObject tmpFbo(m_fbo->size(), format); + QOpenGLFramebufferObject::blitFramebuffer(&tmpFbo, m_fbo); + QOpenGLFramebufferObject::blitFramebuffer(targetFbo, &tmpFbo); +} + +void PenLayerPainter::synchronize(QNanoQuickItem *item) +{ + IPenLayer *penLayer = dynamic_cast(item); + Q_ASSERT(penLayer); + + if (penLayer) + m_fbo = penLayer->framebufferObject(); +} diff --git a/src/penlayerpainter.h b/src/penlayerpainter.h new file mode 100644 index 0000000..f6d6444 --- /dev/null +++ b/src/penlayerpainter.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +#include "texture.h" + +namespace scratchcpprender +{ + +class PenLayerPainter : public QNanoQuickItemPainter +{ + public: + PenLayerPainter(QOpenGLFramebufferObject *fbo = nullptr); + + void paint(QNanoPainter *painter) override; + void synchronize(QNanoQuickItem *item) override; + + private: + QOpenGLFramebufferObject *m_targetFbo = nullptr; + QOpenGLFramebufferObject *m_fbo = nullptr; +}; + +} // namespace scratchcpprender diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp index 9064dda..95a0f8a 100644 --- a/src/spritemodel.cpp +++ b/src/spritemodel.cpp @@ -5,6 +5,7 @@ #include "spritemodel.h" #include "renderedtarget.h" +#include "ipenlayer.h" namespace scratchcpprender { @@ -31,6 +32,9 @@ void SpriteModel::onCloned(libscratchcpp::Sprite *clone) SpriteModel *cloneModel = new SpriteModel(m_cloneRoot); cloneModel->m_cloneRoot = m_cloneRoot; + cloneModel->m_penLayer = m_penLayer; + cloneModel->m_penAttributes = m_penAttributes; + cloneModel->m_penDown = m_penDown; clone->setInterface(cloneModel); emit cloned(cloneModel); } @@ -59,6 +63,12 @@ void SpriteModel::onYChanged(double y) m_renderedTarget->updateY(y); } +void SpriteModel::onMoved(double oldX, double oldY, double newX, double newY) +{ + if (m_penDown && m_penLayer) + m_penLayer->drawLine(m_penAttributes, oldX, oldY, newX, newY); +} + void SpriteModel::onSizeChanged(double size) { if (m_renderedTarget) @@ -115,6 +125,41 @@ void SpriteModel::setRenderedTarget(IRenderedTarget *newRenderedTarget) emit renderedTargetChanged(); } +IPenLayer *SpriteModel::penLayer() const +{ + return m_penLayer; +} + +void SpriteModel::setPenLayer(IPenLayer *newPenLayer) +{ + if (m_penLayer == newPenLayer) + return; + + m_penLayer = newPenLayer; + emit penLayerChanged(); +} + +PenAttributes &SpriteModel::penAttributes() +{ + return m_penAttributes; +} + +bool SpriteModel::penDown() const +{ + return m_penDown; +} + +void SpriteModel::setPenDown(bool newPenDown) +{ + if (m_penDown == newPenDown) + return; + + m_penDown = newPenDown; + + if (m_penDown && m_penLayer && m_sprite) + m_penLayer->drawPoint(m_penAttributes, m_sprite->x(), m_sprite->y()); +} + SpriteModel *SpriteModel::cloneRoot() const { if (m_cloneRoot == this) diff --git a/src/spritemodel.h b/src/spritemodel.h index 8be730b..64473f0 100644 --- a/src/spritemodel.h +++ b/src/spritemodel.h @@ -6,12 +6,16 @@ #include #include +#include "penattributes.h" + Q_MOC_INCLUDE("renderedtarget.h"); +Q_MOC_INCLUDE("ipenlayer.h"); namespace scratchcpprender { class IRenderedTarget; +class IPenLayer; class SpriteModel : public QObject @@ -20,6 +24,7 @@ class SpriteModel Q_OBJECT QML_ELEMENT Q_PROPERTY(IRenderedTarget *renderedTarget READ renderedTarget WRITE setRenderedTarget NOTIFY renderedTargetChanged) + Q_PROPERTY(IPenLayer *penLayer READ penLayer WRITE setPenLayer NOTIFY penLayerChanged) public: SpriteModel(QObject *parent = nullptr); @@ -34,6 +39,7 @@ class SpriteModel void onVisibleChanged(bool visible) override; void onXChanged(double x) override; void onYChanged(double y) override; + void onMoved(double oldX, double oldY, double newX, double newY) override; void onSizeChanged(double size) override; void onDirectionChanged(double direction) override; void onRotationStyleChanged(libscratchcpp::Sprite::RotationStyle rotationStyle) override; @@ -49,16 +55,28 @@ class SpriteModel IRenderedTarget *renderedTarget() const; void setRenderedTarget(IRenderedTarget *newRenderedTarget); + IPenLayer *penLayer() const; + void setPenLayer(IPenLayer *newPenLayer); + + PenAttributes &penAttributes(); + + bool penDown() const; + void setPenDown(bool newPenDown); + SpriteModel *cloneRoot() const; signals: void renderedTargetChanged(); + void penLayerChanged(); void cloned(SpriteModel *cloneModel); void cloneDeleted(SpriteModel *clone); private: libscratchcpp::Sprite *m_sprite = nullptr; IRenderedTarget *m_renderedTarget = nullptr; + IPenLayer *m_penLayer = nullptr; + PenAttributes m_penAttributes; + bool m_penDown = false; SpriteModel *m_cloneRoot = nullptr; }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7be095e..0dbcc55 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -31,3 +31,6 @@ add_subdirectory(scenemousearea) add_subdirectory(monitor_models) add_subdirectory(texture) add_subdirectory(skins) +add_subdirectory(penattributes) +add_subdirectory(penlayer) +add_subdirectory(penlayerpainter) diff --git a/test/lines.png b/test/lines.png new file mode 100644 index 0000000..3cb9ded Binary files /dev/null and b/test/lines.png differ diff --git a/test/lines_old.png b/test/lines_old.png new file mode 100644 index 0000000..3cb9ded Binary files /dev/null and b/test/lines_old.png differ diff --git a/test/mocks/penlayermock.h b/test/mocks/penlayermock.h new file mode 100644 index 0000000..906d634 --- /dev/null +++ b/test/mocks/penlayermock.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +using namespace scratchcpprender; + +namespace scratchcpprender +{ + +class PenLayerMock : public IPenLayer +{ + public: + MOCK_METHOD(bool, antialiasingEnabled, (), (const, override)); + MOCK_METHOD(void, setAntialiasingEnabled, (bool), (override)); + + MOCK_METHOD(libscratchcpp::IEngine *, engine, (), (const, override)); + MOCK_METHOD(void, setEngine, (libscratchcpp::IEngine *), (override)); + + MOCK_METHOD(void, clear, (), (override)); + MOCK_METHOD(void, drawPoint, (const PenAttributes &, double, double), (override)); + MOCK_METHOD(void, drawLine, (const PenAttributes &, double, double, double, double), (override)); + + MOCK_METHOD(QOpenGLFramebufferObject *, framebufferObject, (), (const, override)); + + MOCK_METHOD(QNanoQuickItemPainter *, createItemPainter, (), (const, override)); +}; + +} // namespace scratchcpprender diff --git a/test/penattributes/CMakeLists.txt b/test/penattributes/CMakeLists.txt new file mode 100644 index 0000000..520ad62 --- /dev/null +++ b/test/penattributes/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable( + penattributes_test + penattributes_test.cpp +) + +target_link_libraries( + penattributes_test + GTest::gtest_main + scratchcpp-render + qnanopainter +) + +add_test(penattributes_test) +gtest_discover_tests(penattributes_test) diff --git a/test/penattributes/penattributes_test.cpp b/test/penattributes/penattributes_test.cpp new file mode 100644 index 0000000..a6dc0b9 --- /dev/null +++ b/test/penattributes/penattributes_test.cpp @@ -0,0 +1,15 @@ +#include + +#include "../common.h" + +using namespace scratchcpprender; + +TEST(PenAttributesTest, DefaultPenAttributes) +{ + PenAttributes attr; + ASSERT_EQ(attr.color.redF(), 0); + ASSERT_EQ(attr.color.greenF(), 0); + ASSERT_EQ(attr.color.blueF(), 1); + ASSERT_EQ(attr.color.alphaF(), 1); + ASSERT_EQ(attr.diameter, 1); +} diff --git a/test/penlayer/CMakeLists.txt b/test/penlayer/CMakeLists.txt new file mode 100644 index 0000000..5ad79f2 --- /dev/null +++ b/test/penlayer/CMakeLists.txt @@ -0,0 +1,16 @@ +add_executable( + penlayer_test + penlayer_test.cpp +) + +target_link_libraries( + penlayer_test + GTest::gtest_main + GTest::gmock_main + scratchcpp-render + scratchcpprender_mocks + ${QT_LIBS} +) + +add_test(penlayer_test) +gtest_discover_tests(penlayer_test) diff --git a/test/penlayer/penlayer_test.cpp b/test/penlayer/penlayer_test.cpp new file mode 100644 index 0000000..90beb67 --- /dev/null +++ b/test/penlayer/penlayer_test.cpp @@ -0,0 +1,255 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +using ::testing::Return; + +class PenLayerTest : 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); + } + + void TearDown() override + { + ASSERT_EQ(m_context.surface(), &m_surface); + m_context.doneCurrent(); + } + + QOpenGLContext m_context; + QOffscreenSurface m_surface; +}; + +TEST_F(PenLayerTest, Constructors) +{ + PenLayer penLayer1; + PenLayer penLayer2(&penLayer1); + ASSERT_EQ(penLayer2.parent(), &penLayer1); + ASSERT_EQ(penLayer2.parentItem(), &penLayer1); +} + +TEST_F(PenLayerTest, Engine) +{ + PenLayer penLayer; + ASSERT_EQ(penLayer.engine(), nullptr); + + EngineMock engine1, engine2; + EXPECT_CALL(engine1, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine1, stageHeight()).WillOnce(Return(360)); + penLayer.setEngine(&engine1); + ASSERT_EQ(penLayer.engine(), &engine1); + + EXPECT_CALL(engine2, stageWidth()).WillOnce(Return(500)); + EXPECT_CALL(engine2, stageHeight()).WillOnce(Return(400)); + penLayer.setEngine(&engine2); + ASSERT_EQ(penLayer.engine(), &engine2); + + penLayer.setEngine(nullptr); + ASSERT_EQ(penLayer.engine(), nullptr); +} + +TEST_F(PenLayerTest, FramebufferObject) +{ + PenLayer penLayer; + ASSERT_TRUE(penLayer.antialiasingEnabled()); + + EngineMock engine1, engine2; + EXPECT_CALL(engine1, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine1, stageHeight()).WillOnce(Return(360)); + penLayer.setEngine(&engine1); + + QOpenGLFramebufferObject *fbo = penLayer.framebufferObject(); + ASSERT_EQ(fbo->width(), 480); + ASSERT_EQ(fbo->height(), 360); + ASSERT_EQ(fbo->format().attachment(), QOpenGLFramebufferObject::CombinedDepthStencil); + ASSERT_EQ(fbo->format().samples(), 4); + + penLayer.setAntialiasingEnabled(false); + ASSERT_FALSE(penLayer.antialiasingEnabled()); + + EXPECT_CALL(engine2, stageWidth()).WillOnce(Return(500)); + EXPECT_CALL(engine2, stageHeight()).WillOnce(Return(400)); + penLayer.setEngine(&engine2); + + fbo = penLayer.framebufferObject(); + ASSERT_EQ(fbo->width(), 500); + ASSERT_EQ(fbo->height(), 400); + ASSERT_EQ(fbo->format().attachment(), QOpenGLFramebufferObject::CombinedDepthStencil); + ASSERT_EQ(fbo->format().samples(), 0); +} + +TEST_F(PenLayerTest, GetProjectPenLayer) +{ + PenLayer penLayer; + ASSERT_EQ(penLayer.getProjectPenLayer(nullptr), nullptr); + + EngineMock engine1, engine2; + ASSERT_EQ(penLayer.getProjectPenLayer(&engine1), nullptr); + ASSERT_EQ(penLayer.getProjectPenLayer(&engine2), nullptr); + + EXPECT_CALL(engine1, stageWidth()).WillOnce(Return(1)); + EXPECT_CALL(engine1, stageHeight()).WillOnce(Return(1)); + penLayer.setEngine(&engine1); + ASSERT_EQ(penLayer.getProjectPenLayer(&engine1), &penLayer); + ASSERT_EQ(penLayer.getProjectPenLayer(&engine2), nullptr); + + EXPECT_CALL(engine2, stageWidth()).WillOnce(Return(1)); + EXPECT_CALL(engine2, stageHeight()).WillOnce(Return(1)); + penLayer.setEngine(&engine2); + ASSERT_EQ(penLayer.getProjectPenLayer(&engine1), nullptr); + ASSERT_EQ(penLayer.getProjectPenLayer(&engine2), &penLayer); + + penLayer.setEngine(nullptr); + ASSERT_EQ(penLayer.getProjectPenLayer(&engine1), nullptr); + ASSERT_EQ(penLayer.getProjectPenLayer(&engine2), nullptr); +} + +TEST_F(PenLayerTest, Clear) +{ + PenLayer penLayer; + EngineMock engine; + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + penLayer.setEngine(&engine); + + QOpenGLExtraFunctions glF(&m_context); + glF.initializeOpenGLFunctions(); + + QOpenGLFramebufferObject *fbo = penLayer.framebufferObject(); + QImage image1 = fbo->toImage(); + + // The initial texture must contain only fully transparent pixels + for (int y = 0; y < image1.height(); y++) { + for (int x = 0; x < image1.width(); x++) + ASSERT_EQ(QColor::fromRgba(image1.pixel(x, y)).alphaF(), 0); + } + + // Paint something first to test clear() + fbo->bind(); + QNanoPainter painter; + painter.beginFrame(fbo->width(), fbo->height()); + painter.setStrokeStyle(QNanoColor(0, 0, 0)); + painter.moveTo(0, 0); + painter.lineTo(fbo->width(), fbo->height()); + painter.stroke(); + painter.endFrame(); + fbo->release(); + + penLayer.clear(); + QImage image2 = fbo->toImage(); + + // The image must contain only fully transparent pixels + for (int y = 0; y < image2.height(); y++) { + for (int x = 0; x < image2.width(); x++) + ASSERT_EQ(QColor::fromRgba(image2.pixel(x, y)).alphaF(), 0); + } +} + +TEST_F(PenLayerTest, DrawPoint) +{ + PenLayer penLayer; + penLayer.setAntialiasingEnabled(false); + EngineMock engine; + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + penLayer.setEngine(&engine); + + PenAttributes attr; + attr.color = QColor(255, 0, 0); + attr.diameter = 3; + + EXPECT_CALL(engine, stageWidth()).Times(3).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).Times(3).WillRepeatedly(Return(360)); + penLayer.drawPoint(attr, 63, 164); + penLayer.drawPoint(attr, -56, 93); + penLayer.drawPoint(attr, 130, 77); + + attr.color = QColor(0, 128, 0, 128); + attr.diameter = 10; + + EXPECT_CALL(engine, stageWidth()).Times(3).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).Times(3).WillRepeatedly(Return(360)); + penLayer.drawPoint(attr, 152, -158); + penLayer.drawPoint(attr, -228, 145); + penLayer.drawPoint(attr, -100, 139); + + attr.color = QColor(255, 50, 200, 185); + attr.diameter = 25.6; + + EXPECT_CALL(engine, stageWidth()).Times(3).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).Times(3).WillRepeatedly(Return(360)); + penLayer.drawPoint(attr, -11, 179); + penLayer.drawPoint(attr, 90, -48); + penLayer.drawPoint(attr, -54, 21); + + QOpenGLFramebufferObject *fbo = penLayer.framebufferObject(); + QImage image = fbo->toImage(); + QBuffer buffer; + image.save(&buffer, "png"); + QFile ref("points.png"); + ref.open(QFile::ReadOnly); + buffer.open(QFile::ReadOnly); + ASSERT_EQ(ref.readAll(), buffer.readAll()); +} + +TEST_F(PenLayerTest, DrawLine) +{ + PenLayer penLayer; + penLayer.setAntialiasingEnabled(false); + EngineMock engine; + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + penLayer.setEngine(&engine); + + PenAttributes attr; + attr.color = QColor(255, 0, 0); + attr.diameter = 3; + + EXPECT_CALL(engine, stageWidth()).Times(2).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).Times(2).WillRepeatedly(Return(360)); + penLayer.drawLine(attr, 63, 164, -56, 93); + penLayer.drawLine(attr, 130, 77, 125, -22); + + attr.color = QColor(0, 128, 0, 128); + attr.diameter = 10; + + EXPECT_CALL(engine, stageWidth()).Times(2).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).Times(2).WillRepeatedly(Return(360)); + penLayer.drawLine(attr, 152, -158, -228, 145); + penLayer.drawLine(attr, -100, 139, 20, 72); + + attr.color = QColor(255, 50, 200, 185); + attr.diameter = 25.6; + + EXPECT_CALL(engine, stageWidth()).Times(2).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).Times(2).WillRepeatedly(Return(360)); + penLayer.drawLine(attr, -11, 179, 90, -48); + penLayer.drawLine(attr, -54, 21, 88, -6); + + QOpenGLFramebufferObject *fbo = penLayer.framebufferObject(); + QImage image = fbo->toImage().scaled(240, 180); + QBuffer buffer; + image.save(&buffer, "png"); + QFile ref("lines.png"); + ref.open(QFile::ReadOnly); + buffer.open(QFile::ReadOnly); + ASSERT_EQ(ref.readAll(), buffer.readAll()); +} diff --git a/test/penlayerpainter/CMakeLists.txt b/test/penlayerpainter/CMakeLists.txt new file mode 100644 index 0000000..7de3cd4 --- /dev/null +++ b/test/penlayerpainter/CMakeLists.txt @@ -0,0 +1,17 @@ +add_executable( + penlayerpainter_test + penlayerpainter_test.cpp +) + +target_link_libraries( + penlayerpainter_test + GTest::gtest_main + GTest::gmock_main + scratchcpp-render + scratchcpprender_mocks + ${QT_LIBS} + qnanopainter +) + +add_test(penlayerpainter_test) +gtest_discover_tests(penlayerpainter_test) diff --git a/test/penlayerpainter/penlayerpainter_test.cpp b/test/penlayerpainter/penlayerpainter_test.cpp new file mode 100644 index 0000000..eae84a8 --- /dev/null +++ b/test/penlayerpainter/penlayerpainter_test.cpp @@ -0,0 +1,81 @@ +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; + +using ::testing::Return; +using ::testing::ReturnRef; + +class PenLayerPainterTest : public testing::Test +{ + public: + void createContextAndSurface(QOpenGLContext *context, QOffscreenSurface *surface) + { + QSurfaceFormat surfaceFormat; + surfaceFormat.setMajorVersion(4); + surfaceFormat.setMinorVersion(3); + + context->setFormat(surfaceFormat); + context->create(); + ASSERT_TRUE(context->isValid()); + + surface->setFormat(surfaceFormat); + surface->create(); + ASSERT_TRUE(surface->isValid()); + + context->makeCurrent(surface); + ASSERT_EQ(QOpenGLContext::currentContext(), context); + } +}; + +TEST_F(PenLayerPainterTest, Paint) +{ + QOpenGLContext context; + QOffscreenSurface surface; + createContextAndSurface(&context, &surface); + + 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 + 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(); + + // Begin painting + QNanoPainter painter; + QOpenGLFramebufferObject fbo(40, 60, format); + fbo.bind(); + painter.beginFrame(fbo.width(), fbo.height()); + + // Create pen layer painter + PenLayerPainter penLayerPainter(&fbo); + PenLayerMock penLayer; + + EXPECT_CALL(penLayer, framebufferObject()).WillOnce(Return(&refFbo)); + penLayerPainter.synchronize(&penLayer); + + // Paint + Texture texture(refFbo.texture(), refFbo.size()); + penLayerPainter.paint(&painter); + painter.endFrame(); + + // Compare resulting images + ASSERT_EQ(fbo.toImage(), refFbo.toImage()); + + // Release + fbo.release(); + refFbo.release(); + + context.doneCurrent(); +} diff --git a/test/points.png b/test/points.png new file mode 100644 index 0000000..03a886a Binary files /dev/null and b/test/points.png differ diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp index f5527d1..819aa7e 100644 --- a/test/target_models/spritemodel_test.cpp +++ b/test/target_models/spritemodel_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "../common.h" @@ -9,6 +10,9 @@ using namespace scratchcpprender; using namespace libscratchcpp; using ::testing::Return; +using ::testing::WithArgs; +using ::testing::Invoke; +using ::testing::_; TEST(SpriteModelTest, Constructors) { @@ -70,9 +74,16 @@ TEST(SpriteModelTest, OnCloned) ASSERT_EQ(cloneModel->parent(), &model); ASSERT_EQ(cloneModel->sprite(), &clone2); ASSERT_EQ(cloneModel->cloneRoot(), &model); + ASSERT_FALSE(cloneModel->penDown()); Sprite clone3; QSignalSpy spy2(cloneModel, &SpriteModel::cloned); + PenLayerMock penLayer; + cloneModel->setPenLayer(&penLayer); + cloneModel->penAttributes().color = QColor(255, 0, 0); + cloneModel->penAttributes().diameter = 20.3; + EXPECT_CALL(penLayer, drawPoint); + cloneModel->setPenDown(true); cloneModel->onCloned(&clone3); ASSERT_EQ(spy2.count(), 1); @@ -83,6 +94,10 @@ TEST(SpriteModelTest, OnCloned) ASSERT_EQ(cloneModel->parent(), &model); ASSERT_EQ(cloneModel->sprite(), &clone3); ASSERT_EQ(cloneModel->cloneRoot(), &model); + ASSERT_EQ(cloneModel->penLayer(), &penLayer); + ASSERT_EQ(cloneModel->penAttributes().color, QColor(255, 0, 0)); + ASSERT_EQ(cloneModel->penAttributes().diameter, 20.3); + ASSERT_TRUE(cloneModel->penDown()); } TEST(SpriteModelTest, OnCostumeChanged) @@ -131,6 +146,23 @@ TEST(SpriteModelTest, OnYChanged) model.onYChanged(-46.1); } +TEST(SpriteModelTest, OnMoved) +{ + SpriteModel model; + + PenLayerMock penLayer; + model.setPenLayer(&penLayer); + + EXPECT_CALL(penLayer, drawLine).Times(0); + model.onMoved(-15.6, 54.9, 159.04, -2.5); + + model.setPenDown(true); + PenAttributes &attr = model.penAttributes(); + + EXPECT_CALL(penLayer, drawLine(_, -15.6, 54.9, 159.04, -2.5)).WillOnce(WithArgs<0>(Invoke([&attr](const PenAttributes &attrArg) { ASSERT_EQ(&attr, &attrArg); }))); + model.onMoved(-15.6, 54.9, 159.04, -2.5); +} + TEST(SpriteModelTest, OnSizeChanged) { SpriteModel model; @@ -204,3 +236,42 @@ TEST(SpriteModelTest, RenderedTarget) ASSERT_EQ(spy.count(), 1); ASSERT_EQ(model.renderedTarget(), &renderedTarget); } + +TEST(SpriteModelTest, PenLayer) +{ + SpriteModel model; + ASSERT_EQ(model.penLayer(), nullptr); + + PenLayerMock penLayer; + QSignalSpy spy(&model, &SpriteModel::penLayerChanged); + model.setPenLayer(&penLayer); + ASSERT_EQ(spy.count(), 1); + ASSERT_EQ(model.penLayer(), &penLayer); +} + +TEST(SpriteModelTest, PenDown) +{ + SpriteModel model; + Sprite sprite; + sprite.setX(24.6); + sprite.setY(-48.8); + model.init(&sprite); + ASSERT_FALSE(model.penDown()); + + PenLayerMock penLayer; + model.setPenLayer(&penLayer); + + PenAttributes &attr = model.penAttributes(); + + EXPECT_CALL(penLayer, drawPoint(_, 24.6, -48.8)).WillOnce(WithArgs<0>(Invoke([&attr](const PenAttributes &attrArg) { ASSERT_EQ(&attr, &attrArg); }))); + model.setPenDown(true); + ASSERT_TRUE(model.penDown()); + + EXPECT_CALL(penLayer, drawPoint).Times(0); + model.setPenDown(true); + ASSERT_TRUE(model.penDown()); + + EXPECT_CALL(penLayer, drawPoint).Times(0); + model.setPenDown(false); + ASSERT_FALSE(model.penDown()); +}