diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d2b4c85..946a1f7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -55,6 +55,10 @@ qt_add_qml_module(scratchcpp-render penlayerpainter.cpp penlayerpainter.h penattributes.h + blocks/penextension.cpp + blocks/penextension.h + blocks/penblocks.cpp + blocks/penblocks.h ) list(APPEND QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/src/blocks/penblocks.cpp b/src/blocks/penblocks.cpp new file mode 100644 index 0000000..e314b7d --- /dev/null +++ b/src/blocks/penblocks.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include + +#include "penblocks.h" +#include "penlayer.h" +#include "spritemodel.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +std::string PenBlocks::name() const +{ + return "Pen"; +} + +void PenBlocks::registerBlocks(IEngine *engine) +{ + // Blocks + engine->addCompileFunction(this, "pen_clear", &compileClear); + engine->addCompileFunction(this, "pen_penDown", &compilePenDown); + engine->addCompileFunction(this, "pen_penUp", &compilePenUp); +} + +void PenBlocks::compileClear(Compiler *compiler) +{ + compiler->addFunctionCall(&clear); +} + +void PenBlocks::compilePenDown(Compiler *compiler) +{ + compiler->addFunctionCall(&penDown); +} + +void PenBlocks::compilePenUp(Compiler *compiler) +{ + compiler->addFunctionCall(&penUp); +} + +unsigned int PenBlocks::clear(VirtualMachine *vm) +{ + IPenLayer *penLayer = PenLayer::getProjectPenLayer(vm->engine()); + + if (penLayer) { + penLayer->clear(); + vm->engine()->requestRedraw(); + } + + return 0; +} + +unsigned int PenBlocks::penDown(VirtualMachine *vm) +{ + Target *target = vm->target(); + + if (!target || target->isStage()) + return 0; + + Sprite *sprite = static_cast(target); + SpriteModel *model = static_cast(sprite->getInterface()); + + if (model) + model->setPenDown(true); + + return 0; +} + +unsigned int PenBlocks::penUp(libscratchcpp::VirtualMachine *vm) +{ + Target *target = vm->target(); + + if (!target || target->isStage()) + return 0; + + Sprite *sprite = static_cast(target); + SpriteModel *model = static_cast(sprite->getInterface()); + + if (model) + model->setPenDown(false); + + return 0; +} diff --git a/src/blocks/penblocks.h b/src/blocks/penblocks.h new file mode 100644 index 0000000..7da4ba1 --- /dev/null +++ b/src/blocks/penblocks.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +namespace scratchcpprender +{ + +class PenBlocks : public libscratchcpp::IBlockSection +{ + public: + enum Inputs + { + }; + + std::string name() const override; + + void registerBlocks(libscratchcpp::IEngine *engine) override; + + static void compileClear(libscratchcpp::Compiler *compiler); + static void compilePenDown(libscratchcpp::Compiler *compiler); + static void compilePenUp(libscratchcpp::Compiler *compiler); + + static unsigned int clear(libscratchcpp::VirtualMachine *vm); + static unsigned int penDown(libscratchcpp::VirtualMachine *vm); + static unsigned int penUp(libscratchcpp::VirtualMachine *vm); +}; + +} // namespace scratchcpprender diff --git a/src/blocks/penextension.cpp b/src/blocks/penextension.cpp new file mode 100644 index 0000000..1226d08 --- /dev/null +++ b/src/blocks/penextension.cpp @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include "penextension.h" +#include "penblocks.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +std::string PenExtension::name() const +{ + return "pen"; +} + +std::string PenExtension::description() const +{ + return "Pen extension"; +} + +void PenExtension::registerSections(IEngine *engine) +{ + engine->registerSection(std::make_shared()); +} diff --git a/src/blocks/penextension.h b/src/blocks/penextension.h new file mode 100644 index 0000000..1e7363d --- /dev/null +++ b/src/blocks/penextension.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +namespace scratchcpprender +{ + +class PenExtension : public libscratchcpp::IExtension +{ + public: + std::string name() const override; + std::string description() const override; + + void registerSections(libscratchcpp::IEngine *engine) override; +}; + +} // namespace scratchcpprender diff --git a/src/penlayer.cpp b/src/penlayer.cpp index 0c559e7..2809806 100644 --- a/src/penlayer.cpp +++ b/src/penlayer.cpp @@ -47,7 +47,7 @@ void PenLayer::setEngine(libscratchcpp::IEngine *newEngine) m_engine = newEngine; - if (m_engine) { + if (m_engine && QOpenGLContext::currentContext()) { m_projectPenLayers[m_engine] = this; m_fbo = std::make_unique(m_engine->stageWidth(), m_engine->stageHeight(), m_fboFormat); Q_ASSERT(m_fbo->isValid()); @@ -134,6 +134,11 @@ IPenLayer *PenLayer::getProjectPenLayer(libscratchcpp::IEngine *engine) return nullptr; } +void PenLayer::addPenLayer(libscratchcpp::IEngine *engine, IPenLayer *penLayer) +{ + m_projectPenLayers[engine] = penLayer; +} + QNanoQuickItemPainter *PenLayer::createItemPainter() const { return new PenLayerPainter; diff --git a/src/penlayer.h b/src/penlayer.h index fc88ae2..7a98a0b 100644 --- a/src/penlayer.h +++ b/src/penlayer.h @@ -34,6 +34,7 @@ class PenLayer : public IPenLayer QOpenGLFramebufferObject *framebufferObject() const override; static IPenLayer *getProjectPenLayer(libscratchcpp::IEngine *engine); + static void addPenLayer(libscratchcpp::IEngine *engine, IPenLayer *penLayer); // for tests signals: void engineChanged(); diff --git a/src/projectloader.cpp b/src/projectloader.cpp index fe700ec..c9d9b96 100644 --- a/src/projectloader.cpp +++ b/src/projectloader.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -11,6 +12,7 @@ #include "valuemonitormodel.h" #include "listmonitormodel.h" #include "renderedtarget.h" +#include "blocks/penextension.h" using namespace scratchcpprender; using namespace libscratchcpp; @@ -31,6 +33,9 @@ ProjectLoader::ProjectLoader(QObject *parent) : }); initTimer(); + + // Register pen blocks + ScratchConfiguration::registerExtension(std::make_shared()); } ProjectLoader::~ProjectLoader() diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp index 95a0f8a..a443f99 100644 --- a/src/spritemodel.cpp +++ b/src/spritemodel.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "spritemodel.h" #include "renderedtarget.h" @@ -65,8 +66,13 @@ void SpriteModel::onYChanged(double y) void SpriteModel::onMoved(double oldX, double oldY, double newX, double newY) { - if (m_penDown && m_penLayer) + if (m_penDown && m_penLayer) { m_penLayer->drawLine(m_penAttributes, oldX, oldY, newX, newY); + libscratchcpp::IEngine *engine = m_sprite->engine(); + + if (engine) + engine->requestRedraw(); + } } void SpriteModel::onSizeChanged(double size) @@ -151,13 +157,15 @@ bool SpriteModel::penDown() const void SpriteModel::setPenDown(bool newPenDown) { - if (m_penDown == newPenDown) - return; - m_penDown = newPenDown; - if (m_penDown && m_penLayer && m_sprite) + if (m_penDown && m_penLayer && m_sprite) { m_penLayer->drawPoint(m_penAttributes, m_sprite->x(), m_sprite->y()); + libscratchcpp::IEngine *engine = m_sprite->engine(); + + if (engine) + engine->requestRedraw(); + } } SpriteModel *SpriteModel::cloneRoot() const diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0dbcc55..85eaac7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -34,3 +34,4 @@ add_subdirectory(skins) add_subdirectory(penattributes) add_subdirectory(penlayer) add_subdirectory(penlayerpainter) +add_subdirectory(blocks) diff --git a/test/blocks/CMakeLists.txt b/test/blocks/CMakeLists.txt new file mode 100644 index 0000000..6d6151d --- /dev/null +++ b/test/blocks/CMakeLists.txt @@ -0,0 +1,35 @@ +# pen_blocks_test +add_executable( + pen_blocks_test + pen_blocks_test.cpp +) + +target_link_libraries( + pen_blocks_test + GTest::gtest_main + GTest::gmock_main + scratchcpp-render + scratchcpprender_mocks + ${QT_LIBS} +) + +add_test(pen_blocks_test) +gtest_discover_tests(pen_blocks_test) + +# penextension_test +add_executable( + penextension_test + penextension_test.cpp +) + +target_link_libraries( + penextension_test + GTest::gtest_main + GTest::gmock_main + scratchcpp-render + scratchcpprender_mocks + ${QT_LIBS} +) + +add_test(penextension_test) +gtest_discover_tests(penextension_test) diff --git a/test/blocks/pen_blocks_test.cpp b/test/blocks/pen_blocks_test.cpp new file mode 100644 index 0000000..e6d9f66 --- /dev/null +++ b/test/blocks/pen_blocks_test.cpp @@ -0,0 +1,172 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +using ::testing::Return; + +class PenBlocksTest : public testing::Test +{ + public: + void SetUp() override { m_section = std::make_unique(); } + + void addValueInput(std::shared_ptr block, const std::string &name, PenBlocks::Inputs id, const Value &value) const + { + auto input = std::make_shared(name, Input::Type::Shadow); + input->setPrimaryValue(value); + input->setInputId(id); + block->addInput(input); + } + + std::unique_ptr m_section; + EngineMock m_engineMock; +}; + +TEST_F(PenBlocksTest, Name) +{ + ASSERT_EQ(m_section->name(), "Pen"); +} + +TEST_F(PenBlocksTest, CategoryVisible) +{ + ASSERT_TRUE(m_section->categoryVisible()); +} + +TEST_F(PenBlocksTest, RegisterBlocks) +{ + // Blocks + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_clear", &PenBlocks::compileClear)); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_penDown", &PenBlocks::compilePenDown)); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_penUp", &PenBlocks::compilePenUp)); + + m_section->registerBlocks(&m_engineMock); +} + +TEST_F(PenBlocksTest, Clear) +{ + Compiler compiler(&m_engineMock); + + auto block = std::make_shared("a", "pen_clear"); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::clear)).WillOnce(Return(2)); + compiler.init(); + compiler.setBlock(block); + PenBlocks::compileClear(&compiler); + compiler.end(); + + ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_EXEC, 2, vm::OP_HALT })); + ASSERT_TRUE(compiler.constValues().empty()); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(PenBlocksTest, ClearImpl) +{ + static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }; + static BlockFunc functions[] = { &PenBlocks::clear }; + + PenLayerMock penLayer; + PenLayer::addPenLayer(&m_engineMock, &penLayer); + + VirtualMachine vm(nullptr, &m_engineMock, nullptr); + vm.setBytecode(bytecode); + vm.setFunctions(functions); + + EXPECT_CALL(penLayer, clear()); + EXPECT_CALL(m_engineMock, requestRedraw()); + vm.run(); + + ASSERT_EQ(vm.registerCount(), 0); +} + +TEST_F(PenBlocksTest, PenDown) +{ + Compiler compiler(&m_engineMock); + + auto block = std::make_shared("a", "pen_penDown"); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::penDown)).WillOnce(Return(2)); + compiler.init(); + compiler.setBlock(block); + PenBlocks::compilePenDown(&compiler); + compiler.end(); + + ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_EXEC, 2, vm::OP_HALT })); + ASSERT_TRUE(compiler.constValues().empty()); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(PenBlocksTest, PenDownImpl) +{ + static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }; + static BlockFunc functions[] = { &PenBlocks::penDown }; + + SpriteModel model; + Sprite sprite; + sprite.setInterface(&model); + + VirtualMachine vm(&sprite, &m_engineMock, nullptr); + vm.setBytecode(bytecode); + vm.setFunctions(functions); + + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_TRUE(model.penDown()); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_TRUE(model.penDown()); +} + +TEST_F(PenBlocksTest, PenUp) +{ + Compiler compiler(&m_engineMock); + + auto block = std::make_shared("a", "pen_penUp"); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::penUp)).WillOnce(Return(2)); + compiler.init(); + compiler.setBlock(block); + PenBlocks::compilePenUp(&compiler); + compiler.end(); + + ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_EXEC, 2, vm::OP_HALT })); + ASSERT_TRUE(compiler.constValues().empty()); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(PenBlocksTest, PenUpImpl) +{ + static unsigned int bytecode[] = { vm::OP_START, vm::OP_EXEC, 0, vm::OP_HALT }; + static BlockFunc functions[] = { &PenBlocks::penUp }; + + SpriteModel model; + model.setPenDown(true); + Sprite sprite; + sprite.setInterface(&model); + + VirtualMachine vm(&sprite, &m_engineMock, nullptr); + vm.setBytecode(bytecode); + vm.setFunctions(functions); + + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_FALSE(model.penDown()); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_FALSE(model.penDown()); +} diff --git a/test/blocks/penextension_test.cpp b/test/blocks/penextension_test.cpp new file mode 100644 index 0000000..009a497 --- /dev/null +++ b/test/blocks/penextension_test.cpp @@ -0,0 +1,39 @@ +#include +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +using ::testing::WithArgs; +using ::testing::Invoke; +using ::testing::_; + +TEST(PenExtensionTest, Name) +{ + PenExtension ext; + ASSERT_EQ(ext.name(), "pen"); +} + +TEST(PenExtensionTest, Description) +{ + PenExtension ext; + ASSERT_EQ(ext.description(), "Pen extension"); +} + +TEST(PenExtensionTest, IncludeByDefault) +{ + PenExtension ext; + ASSERT_FALSE(ext.includeByDefault()); +} + +TEST(PenExtensionTest, RegisterSections) +{ + PenExtension ext; + EngineMock engine; + + EXPECT_CALL(engine, registerSection(_)).WillOnce(WithArgs<0>(Invoke([](std::shared_ptr section) { ASSERT_TRUE(dynamic_cast(section.get())); }))); + ext.registerSections(&engine); +} diff --git a/test/penlayer/penlayer_test.cpp b/test/penlayer/penlayer_test.cpp index 90beb67..539a7e8 100644 --- a/test/penlayer/penlayer_test.cpp +++ b/test/penlayer/penlayer_test.cpp @@ -99,27 +99,31 @@ TEST_F(PenLayerTest, FramebufferObject) TEST_F(PenLayerTest, GetProjectPenLayer) { PenLayer penLayer; - ASSERT_EQ(penLayer.getProjectPenLayer(nullptr), nullptr); + ASSERT_EQ(PenLayer::getProjectPenLayer(nullptr), nullptr); EngineMock engine1, engine2; - ASSERT_EQ(penLayer.getProjectPenLayer(&engine1), nullptr); - ASSERT_EQ(penLayer.getProjectPenLayer(&engine2), nullptr); + 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); + 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); + 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); + ASSERT_EQ(PenLayer::getProjectPenLayer(&engine1), nullptr); + ASSERT_EQ(PenLayer::getProjectPenLayer(&engine2), nullptr); + + PenLayer::addPenLayer(&engine1, &penLayer); + ASSERT_EQ(PenLayer::getProjectPenLayer(&engine1), &penLayer); + ASSERT_EQ(PenLayer::getProjectPenLayer(&engine2), nullptr); } TEST_F(PenLayerTest, Clear) diff --git a/test/projectloader/projectloader_test.cpp b/test/projectloader/projectloader_test.cpp index 84adb5c..47420f6 100644 --- a/test/projectloader/projectloader_test.cpp +++ b/test/projectloader/projectloader_test.cpp @@ -1,8 +1,10 @@ #include +#include #include #include #include #include +#include #include #include @@ -62,6 +64,9 @@ TEST_F(ProjectLoaderTest, Constructors) ProjectLoader loader1; ProjectLoader loader2(&loader1); ASSERT_EQ(loader2.parent(), &loader1); + + // Pen extension should be registered + ASSERT_TRUE(dynamic_cast(ScratchConfiguration::getExtension("pen"))); } TEST_F(ProjectLoaderTest, Load) diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp index 819aa7e..7d932cd 100644 --- a/test/target_models/spritemodel_test.cpp +++ b/test/target_models/spritemodel_test.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "../common.h" @@ -149,17 +150,25 @@ TEST(SpriteModelTest, OnYChanged) TEST(SpriteModelTest, OnMoved) { SpriteModel model; + Sprite sprite; + EngineMock engine; + sprite.setEngine(&engine); + model.init(&sprite); PenLayerMock penLayer; model.setPenLayer(&penLayer); EXPECT_CALL(penLayer, drawLine).Times(0); + EXPECT_CALL(engine, requestRedraw).Times(0); model.onMoved(-15.6, 54.9, 159.04, -2.5); + EXPECT_CALL(penLayer, drawPoint); + EXPECT_CALL(engine, requestRedraw); 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); }))); + EXPECT_CALL(engine, requestRedraw()); model.onMoved(-15.6, 54.9, 159.04, -2.5); } @@ -253,8 +262,10 @@ TEST(SpriteModelTest, PenDown) { SpriteModel model; Sprite sprite; + EngineMock engine; sprite.setX(24.6); sprite.setY(-48.8); + sprite.setEngine(&engine); model.init(&sprite); ASSERT_FALSE(model.penDown()); @@ -264,14 +275,17 @@ TEST(SpriteModelTest, PenDown) PenAttributes &attr = model.penAttributes(); EXPECT_CALL(penLayer, drawPoint(_, 24.6, -48.8)).WillOnce(WithArgs<0>(Invoke([&attr](const PenAttributes &attrArg) { ASSERT_EQ(&attr, &attrArg); }))); + EXPECT_CALL(engine, requestRedraw()); model.setPenDown(true); ASSERT_TRUE(model.penDown()); - EXPECT_CALL(penLayer, drawPoint).Times(0); + EXPECT_CALL(penLayer, drawPoint(_, 24.6, -48.8)); + EXPECT_CALL(engine, requestRedraw()); model.setPenDown(true); ASSERT_TRUE(model.penDown()); EXPECT_CALL(penLayer, drawPoint).Times(0); + EXPECT_CALL(engine, requestRedraw).Times(0); model.setPenDown(false); ASSERT_FALSE(model.penDown()); }