diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aa7735d..a1366e3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -qt_add_library(scratchcpp-render STATIC) +qt_add_library(scratchcpp-render SHARED) set_target_properties(scratchcpp-render PROPERTIES AUTOMOC ON) @@ -78,6 +78,11 @@ qt_add_qml_module(scratchcpp-render effecttransform.h ) +target_sources(scratchcpp-render + PRIVATE + blocks/penblocks.cpp + blocks/penblocks.h) + list(APPEND QML_IMPORT_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) list(REMOVE_DUPLICATES QML_IMPORT_PATH) set(QML_IMPORT_PATH ${QML_IMPORT_PATH} CACHE STRING "" FORCE) diff --git a/src/blocks/penblocks.cpp b/src/blocks/penblocks.cpp new file mode 100644 index 0000000..ceb7ce6 --- /dev/null +++ b/src/blocks/penblocks.cpp @@ -0,0 +1,443 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "penblocks.h" +#include "penlayer.h" +#include "spritemodel.h" +#include "stagemodel.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +// Pen size range: https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/extensions/scratch3_pen/index.js#L100-L102 +static const double PEN_SIZE_MIN = 1; +static const double PEN_SIZE_MAX = 1200; + +static const double COLOR_PARAM_MIN = 0; +static const double COLOR_PARAM_MAX = 100; + +inline double wrapClamp(double n, double min, double max) +{ + // TODO: Move this to a separate class + const double range = max - min /*+ 1*/; + return n - (std::floor((n - min) / range) * range); +} + +inline QColor pen_convert_from_numeric_color(long color) +{ + return QColor::fromRgba(static_cast(color)); +} + +static void pen_convert_color(const ValueData *color, QColor &dst) +{ + StringPtr *str = string_pool_new(); + + if (value_isString(color)) { + value_toStringPtr(color, str); + + if (str->size > 0 && str->data[0] == u'#') { + if (str->size <= 7) // #RRGGBB + { + dst = QColor::fromString(str->data); + + if (!dst.isValid()) + dst = Qt::black; + } else + dst = Qt::black; + } else + dst = pen_convert_from_numeric_color(value_toLong(color)); + } else + dst = pen_convert_from_numeric_color(value_toLong(color)); + + string_pool_free(str); +} + +std::string PenBlocks::name() const +{ + return "pen"; +} + +std::string PenBlocks::description() const +{ + return name() + " blocks"; +} + +Rgb PenBlocks::color() const +{ + return rgb(15, 189, 140); +} + +void PenBlocks::registerBlocks(IEngine *engine) +{ + engine->addCompileFunction(this, "pen_clear", &compileClear); + engine->addCompileFunction(this, "pen_stamp", &compileStamp); + engine->addCompileFunction(this, "pen_penDown", &compilePenDown); + engine->addCompileFunction(this, "pen_penUp", &compilePenUp); + engine->addCompileFunction(this, "pen_setPenColorToColor", &compileSetPenColorToColor); + engine->addCompileFunction(this, "pen_changePenColorParamBy", &compileChangePenColorParamBy); + engine->addCompileFunction(this, "pen_setPenColorParamTo", &compileSetPenColorParamTo); + engine->addCompileFunction(this, "pen_changePenSizeBy", &compileChangePenSizeBy); + engine->addCompileFunction(this, "pen_setPenSizeTo", &compileSetPenSizeTo); + engine->addCompileFunction(this, "pen_changePenShadeBy", &compileChangePenShadeBy); + engine->addCompileFunction(this, "pen_setPenShadeToNumber", &compileSetPenShadeToNumber); + engine->addCompileFunction(this, "pen_changePenHueBy", &compileChangePenHueBy); + engine->addCompileFunction(this, "pen_setPenHueToNumber", &compileSetPenHueToNumber); +} + +CompilerValue *PenBlocks::compileClear(Compiler *compiler) +{ + compiler->addFunctionCallWithCtx("pen_clear"); + return nullptr; +} + +CompilerValue *PenBlocks::compileStamp(Compiler *compiler) +{ + compiler->addTargetFunctionCall("pen_stamp"); + return nullptr; +} + +CompilerValue *PenBlocks::compilePenDown(Compiler *compiler) +{ + CompilerValue *arg = compiler->addConstValue(true); + compiler->addTargetFunctionCall("pen_set_pen_down", Compiler::StaticType::Void, { Compiler::StaticType::Bool }, { arg }); + return nullptr; +} + +CompilerValue *PenBlocks::compilePenUp(Compiler *compiler) +{ + CompilerValue *arg = compiler->addConstValue(false); + compiler->addTargetFunctionCall("pen_set_pen_down", Compiler::StaticType::Void, { Compiler::StaticType::Bool }, { arg }); + return nullptr; +} + +CompilerValue *PenBlocks::compileSetPenColorToColor(Compiler *compiler) +{ + CompilerValue *color = compiler->addInput("COLOR"); + + if (color->isConst()) { + // Convert color constant at compile time + const ValueData *value = &dynamic_cast(color)->value().data(); + QColor converted; + pen_convert_color(value, converted); + + QColor hsv = converted.toHsv(); + CompilerValue *h = compiler->addConstValue((hsv.hue() / 360.0) * 100); + CompilerValue *s = compiler->addConstValue(hsv.saturationF() * 100); + CompilerValue *b = compiler->addConstValue(hsv.valueF() * 100); + CompilerValue *transparency; + + if (converted.alpha() > 0) + transparency = compiler->addConstValue(100 * (1 - converted.alpha() / 255.0)); + else + transparency = compiler->addConstValue(0); + + compiler->addTargetFunctionCall( + "pen_setPenColorToHsbColor", + Compiler::StaticType::Void, + { Compiler::StaticType::Number, Compiler::StaticType::Number, Compiler::StaticType::Number, Compiler::StaticType::Number }, + { h, s, b, transparency }); + } else + compiler->addTargetFunctionCall("pen_setPenColorToColor", Compiler::StaticType::Void, { Compiler::StaticType::Unknown }, { color }); + + return nullptr; +} + +void PenBlocks::compileSetOrChangePenColorParam(Compiler *compiler, bool change) +{ + Input *paramInput = compiler->input("COLOR_PARAM"); + CompilerValue *param = compiler->addInput(paramInput); + CompilerValue *value = compiler->addInput("VALUE"); + CompilerValue *changeValue = compiler->addConstValue(change); + + if (paramInput->pointsToDropdownMenu()) { + static const std::unordered_set options = { "color", "saturation", "brightness", "transparency" }; + std::string option = paramInput->selectedMenuItem(); + + if (options.find(option) != options.cend()) { + std::string f = "pen_set_or_change_" + option; + compiler->addTargetFunctionCall(f, Compiler::StaticType::Void, { Compiler::StaticType::Number, Compiler::StaticType::Bool }, { value, changeValue }); + } + } else { + compiler->addTargetFunctionCall( + "pen_set_or_change_color_param", + Compiler::StaticType::Void, + { Compiler::StaticType::String, Compiler::StaticType::Number, Compiler::StaticType::Bool }, + { param, value, changeValue }); + } +} + +CompilerValue *PenBlocks::compileChangePenColorParamBy(Compiler *compiler) +{ + compileSetOrChangePenColorParam(compiler, true); + return nullptr; +} + +CompilerValue *PenBlocks::compileSetPenColorParamTo(Compiler *compiler) +{ + compileSetOrChangePenColorParam(compiler, false); + return nullptr; +} + +CompilerValue *PenBlocks::compileChangePenSizeBy(Compiler *compiler) +{ + CompilerValue *size = compiler->addInput("SIZE"); + compiler->addTargetFunctionCall("pen_changePenSizeBy", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { size }); + return nullptr; +} + +CompilerValue *PenBlocks::compileSetPenSizeTo(Compiler *compiler) +{ + CompilerValue *size = compiler->addInput("SIZE"); + compiler->addTargetFunctionCall("pen_setPenSizeTo", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { size }); + return nullptr; +} + +CompilerValue *PenBlocks::compileChangePenShadeBy(Compiler *compiler) +{ + CompilerValue *shade = compiler->addInput("SHADE"); + CompilerValue *change = compiler->addConstValue(true); + compiler->addTargetFunctionCall("pen_set_or_change_pen_shade", Compiler::StaticType::Void, { Compiler::StaticType::Number, Compiler::StaticType::Bool }, { shade, change }); + return nullptr; +} + +CompilerValue *PenBlocks::compileSetPenShadeToNumber(Compiler *compiler) +{ + CompilerValue *shade = compiler->addInput("SHADE"); + CompilerValue *change = compiler->addConstValue(false); + compiler->addTargetFunctionCall("pen_set_or_change_pen_shade", Compiler::StaticType::Void, { Compiler::StaticType::Number, Compiler::StaticType::Bool }, { shade, change }); + return nullptr; +} + +CompilerValue *PenBlocks::compileChangePenHueBy(Compiler *compiler) +{ + CompilerValue *hue = compiler->addInput("HUE"); + CompilerValue *change = compiler->addConstValue(true); + compiler->addTargetFunctionCall("pen_set_or_change_pen_hue", Compiler::StaticType::Void, { Compiler::StaticType::Number, Compiler::StaticType::Bool }, { hue, change }); + return nullptr; +} + +CompilerValue *PenBlocks::compileSetPenHueToNumber(Compiler *compiler) +{ + CompilerValue *hue = compiler->addInput("HUE"); + CompilerValue *transparency = compiler->addConstValue(0); + CompilerValue *change = compiler->addConstValue(false); + + compiler->addTargetFunctionCall("pen_set_or_change_pen_hue", Compiler::StaticType::Void, { Compiler::StaticType::Number, Compiler::StaticType::Bool }, { hue, change }); + compiler->addTargetFunctionCall("pen_set_or_change_transparency", Compiler::StaticType::Void, { Compiler::StaticType::Number, Compiler::StaticType::Bool }, { transparency, change }); + return nullptr; +} + +static TargetModel *getTargetModel(Target *target) +{ + if (target->isStage()) { + Stage *stage = static_cast(target); + return static_cast(stage->getInterface()); + } else { + Sprite *sprite = static_cast(target); + return static_cast(sprite->getInterface()); + } +} + +BLOCK_EXPORT void pen_clear(ExecutionContext *ctx) +{ + IEngine *engine = ctx->engine(); + IPenLayer *penLayer = PenLayer::getProjectPenLayer(engine); + + if (penLayer) { + penLayer->clear(); + engine->requestRedraw(); + } +} + +BLOCK_EXPORT void pen_stamp(Target *target) +{ + IEngine *engine = target->engine(); + IPenLayer *penLayer = PenLayer::getProjectPenLayer(engine); + + if (penLayer) { + IRenderedTarget *renderedTarget = nullptr; + + if (target->isStage()) { + IStageHandler *iface = static_cast(target)->getInterface(); + renderedTarget = static_cast(iface)->renderedTarget(); + } else { + ISpriteHandler *iface = static_cast(target)->getInterface(); + renderedTarget = static_cast(iface)->renderedTarget(); + } + + penLayer->stamp(renderedTarget); + engine->requestRedraw(); + } +} + +BLOCK_EXPORT void pen_set_pen_down(Target *target, bool down) +{ + getTargetModel(target)->setPenDown(down); +} + +BLOCK_EXPORT void pen_setPenColorToHsbColor(Target *target, double h, double s, double b, double transparency) +{ + TargetModel *model = getTargetModel(target); + + PenState &penState = model->penState(); + penState.color = h; + penState.saturation = s; + penState.brightness = b; + penState.transparency = transparency; + + penState.updateColor(); + + // Set the legacy "shade" value the same way Scratch 2 did. + penState.shade = penState.brightness / 2; +} + +BLOCK_EXPORT void pen_setPenColorToColor(Target *target, const ValueData *color) +{ + QColor converted; + pen_convert_color(color, converted); + + QColor hsv = converted.toHsv(); + double h = (hsv.hue() / 360.0) * 100; + double s = hsv.saturationF() * 100; + double b = hsv.valueF() * 100; + double transparency; + + if (converted.alpha() > 0) + transparency = 100 * (1 - converted.alpha() / 255.0); + else + transparency = 0; + + pen_setPenColorToHsbColor(target, h, s, b, transparency); +} + +BLOCK_EXPORT void pen_set_or_change_color(Target *target, double value, bool change) +{ + PenState &penState = getTargetModel(target)->penState(); + penState.color = wrapClamp(value + (change ? penState.color : 0), 0, 100); + penState.updateColor(); +} + +BLOCK_EXPORT void pen_set_or_change_saturation(Target *target, double value, bool change) +{ + PenState &penState = getTargetModel(target)->penState(); + penState.saturation = std::clamp(value + (change ? penState.saturation : 0), COLOR_PARAM_MIN, COLOR_PARAM_MAX); + penState.updateColor(); +} + +BLOCK_EXPORT void pen_set_or_change_brightness(Target *target, double value, bool change) +{ + PenState &penState = getTargetModel(target)->penState(); + penState.brightness = std::clamp(value + (change ? penState.brightness : 0), COLOR_PARAM_MIN, COLOR_PARAM_MAX); + penState.updateColor(); +} + +BLOCK_EXPORT void pen_set_or_change_transparency(Target *target, double value, bool change) +{ + PenState &penState = getTargetModel(target)->penState(); + penState.transparency = std::clamp(value + (change ? penState.transparency : 0), COLOR_PARAM_MIN, COLOR_PARAM_MAX); + penState.updateColor(); +} + +BLOCK_EXPORT void pen_set_or_change_color_param(Target *target, const StringPtr *param, double value, bool change) +{ + static const StringPtr COLOR_PARAM("color"); + static const StringPtr SATURATION_PARAM("saturation"); + static const StringPtr BRIGHTNESS_PARAM("brightness"); + static const StringPtr TRANSPARENCY_PARAM("transparency"); + + if (string_compare_case_sensitive(param, &COLOR_PARAM) == 0) + pen_set_or_change_color(target, value, change); + else if (string_compare_case_sensitive(param, &SATURATION_PARAM) == 0) + pen_set_or_change_saturation(target, value, change); + else if (string_compare_case_sensitive(param, &BRIGHTNESS_PARAM) == 0) + pen_set_or_change_brightness(target, value, change); + else if (string_compare_case_sensitive(param, &TRANSPARENCY_PARAM) == 0) + pen_set_or_change_transparency(target, value, change); +} + +BLOCK_EXPORT void pen_changePenSizeBy(Target *target, double value) +{ + PenAttributes &penAttributes = getTargetModel(target)->penAttributes(); + penAttributes.diameter = std::clamp(penAttributes.diameter + value, PEN_SIZE_MIN, PEN_SIZE_MAX); +} + +BLOCK_EXPORT void pen_setPenSizeTo(Target *target, double value) +{ + PenAttributes &penAttributes = getTargetModel(target)->penAttributes(); + penAttributes.diameter = std::clamp(value, PEN_SIZE_MIN, PEN_SIZE_MAX); +} + +static QRgb mix_rgb(QRgb rgb0, QRgb rgb1, double fraction1) +{ + // https://github.com/scratchfoundation/scratch-vm/blob/a4f095db5e03e072ba222fe721eeeb543c9b9c15/src/util/color.js#L192-L201 + // https://github.com/scratchfoundation/scratch-flash/blob/2e4a402ceb205a042887f54b26eebe1c2e6da6c0/src/util/Color.as#L75-L89 + if (fraction1 <= 0) + return rgb0; + + if (fraction1 >= 1) + return rgb1; + + const double fraction0 = 1 - fraction1; + const int r = static_cast(((fraction0 * qRed(rgb0)) + (fraction1 * qRed(rgb1)))) & 255; + const int g = static_cast(((fraction0 * qGreen(rgb0)) + (fraction1 * qGreen(rgb1)))) & 255; + const int b = static_cast(((fraction0 * qBlue(rgb0)) + (fraction1 * qBlue(rgb1)))) & 255; + return qRgb(r, g, b); +} + +inline void legacy_update_pen_color(PenState &penState) +{ + // https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/extensions/scratch3_pen/index.js#L750-L767 + // Create the new color in RGB using the scratch 2 "shade" model + QRgb rgb = QColor::fromHsvF(penState.color / 100, 1, 1).rgb(); + const double shade = (penState.shade > 100) ? 200 - penState.shade : penState.shade; + + if (shade < 50) + rgb = mix_rgb(0, rgb, (10 + shade) / 60); + else + rgb = mix_rgb(rgb, 0xFFFFFF, (shade - 50) / 60); + + // Update the pen state according to new color + QColor hsv = QColor::fromRgb(rgb).toHsv(); + penState.color = 100 * hsv.hueF(); + penState.saturation = 100 * hsv.saturationF(); + penState.brightness = 100 * hsv.valueF(); + + penState.updateColor(); +} + +BLOCK_EXPORT void pen_set_or_change_pen_shade(Target *target, double shade, bool change) +{ + // https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/extensions/scratch3_pen/index.js#L718-L730 + PenState &penState = getTargetModel(target)->penState(); + + if (change) + shade += penState.shade; + + // Wrap clamp the new shade value the way Scratch 2 did + const double hi = 200.0; + shade = fmod(shade, hi); + + if (shade < 0) + shade += hi; + + // And store the shade that was used to compute this new color for later use + penState.shade = shade; + + legacy_update_pen_color(penState); +} + +BLOCK_EXPORT void pen_set_or_change_pen_hue(Target *target, double hue, bool change) +{ + pen_set_or_change_color(target, hue / 2.0, change); + legacy_update_pen_color(getTargetModel(target)->penState()); +} diff --git a/src/blocks/penblocks.h b/src/blocks/penblocks.h new file mode 100644 index 0000000..10c1f67 --- /dev/null +++ b/src/blocks/penblocks.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +namespace scratchcpprender +{ + +class PenBlocks : public libscratchcpp::IExtension +{ + public: + std::string name() const override; + std::string description() const override; + libscratchcpp::Rgb color() const override; + + void registerBlocks(libscratchcpp::IEngine *engine) override; + + private: + static libscratchcpp::CompilerValue *compileClear(libscratchcpp::Compiler *compiler); + static libscratchcpp::CompilerValue *compileStamp(libscratchcpp::Compiler *compiler); + static libscratchcpp::CompilerValue *compilePenDown(libscratchcpp::Compiler *compiler); + static libscratchcpp::CompilerValue *compilePenUp(libscratchcpp::Compiler *compiler); + static libscratchcpp::CompilerValue *compileSetPenColorToColor(libscratchcpp::Compiler *compiler); + static void compileSetOrChangePenColorParam(libscratchcpp::Compiler *compiler, bool change); + static libscratchcpp::CompilerValue *compileChangePenColorParamBy(libscratchcpp::Compiler *compiler); + static libscratchcpp::CompilerValue *compileSetPenColorParamTo(libscratchcpp::Compiler *compiler); + static libscratchcpp::CompilerValue *compileChangePenSizeBy(libscratchcpp::Compiler *compiler); + static libscratchcpp::CompilerValue *compileSetPenSizeTo(libscratchcpp::Compiler *compiler); + static libscratchcpp::CompilerValue *compileChangePenShadeBy(libscratchcpp::Compiler *compiler); + static libscratchcpp::CompilerValue *compileSetPenShadeToNumber(libscratchcpp::Compiler *compiler); + static libscratchcpp::CompilerValue *compileChangePenHueBy(libscratchcpp::Compiler *compiler); + static libscratchcpp::CompilerValue *compileSetPenHueToNumber(libscratchcpp::Compiler *compiler); +}; + +} // namespace scratchcpprender diff --git a/src/ipenlayer.h b/src/ipenlayer.h index 1ca3fe6..d359bbd 100644 --- a/src/ipenlayer.h +++ b/src/ipenlayer.h @@ -34,6 +34,9 @@ class IPenLayer : public QNanoQuickItem virtual libscratchcpp::IEngine *engine() const = 0; virtual void setEngine(libscratchcpp::IEngine *newEngine) = 0; + virtual void beginFrame() = 0; + virtual void endFrame() = 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; diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h index 662e95b..4d04db8 100644 --- a/src/irenderedtarget.h +++ b/src/irenderedtarget.h @@ -77,6 +77,8 @@ class IRenderedTarget : public QNanoQuickItem virtual bool mirrorHorizontally() const = 0; + virtual void render(double scale) const = 0; + virtual Texture texture() const = 0; virtual const Texture &cpuTexture() const = 0; virtual int costumeWidth() const = 0; diff --git a/src/penlayer.cpp b/src/penlayer.cpp index b93cb30..484ecd0 100644 --- a/src/penlayer.cpp +++ b/src/penlayer.cpp @@ -15,13 +15,6 @@ static const double pi = std::acos(-1); // TODO: Use std::numbers::pi in C++20 std::unordered_map PenLayer::m_projectPenLayers; -// TODO: Move this to a separate class -template -short sgn(T x) -{ - return (T(0) < x) - (x < T(0)); -} - PenLayer::PenLayer(QNanoQuickItem *parent) : IPenLayer(parent) { @@ -37,9 +30,6 @@ PenLayer::~PenLayer() // Delete vertex array and buffer m_glF->glDeleteVertexArrays(1, &m_vao); m_glF->glDeleteBuffers(1, &m_vbo); - - // Delete stamp FBO - m_glF->glDeleteFramebuffers(1, &m_stampFbo); } } @@ -110,17 +100,28 @@ void PenLayer::setEngine(libscratchcpp::IEngine *newEngine) m_glF->glBindVertexArray(0); m_glF->glBindBuffer(GL_ARRAY_BUFFER, 0); - - // Create stamp FBO - m_glF->glGenFramebuffers(1, &m_stampFbo); } + beginFrame(); clear(); + endFrame(); } emit engineChanged(); } +void PenLayer::beginFrame() +{ + m_fbo->bind(); + m_painter->beginFrame(m_fbo->width(), m_fbo->height()); +} + +void PenLayer::endFrame() +{ + m_painter->endFrame(); + m_fbo->release(); +} + bool PenLayer::hqPen() const { return m_hqPen; @@ -141,12 +142,11 @@ void scratchcpprender::PenLayer::clear() if (!m_fbo) return; - m_fbo->bind(); + Q_ASSERT(m_fbo->isBound()); m_glF->glDisable(GL_SCISSOR_TEST); m_glF->glClearColor(0.0f, 0.0f, 0.0f, 0.0f); m_glF->glClear(GL_COLOR_BUFFER_BIT); m_glF->glEnable(GL_SCISSOR_TEST); - m_fbo->release(); m_textureDirty = true; m_boundsDirty = true; @@ -163,10 +163,7 @@ void scratchcpprender::PenLayer::drawLine(const PenAttributes &penAttributes, do if (!m_fbo || !m_painter || !m_engine) return; - // Begin painting - m_fbo->bind(); - - m_painter->beginFrame(m_fbo->width(), m_fbo->height()); + Q_ASSERT(m_fbo->isBound()); // Apply scale (HQ pen) x0 *= m_scale; @@ -205,10 +202,6 @@ void scratchcpprender::PenLayer::drawLine(const PenAttributes &penAttributes, do m_painter->stroke(); } - // End painting - m_painter->endFrame(); - m_fbo->release(); - m_textureDirty = true; m_boundsDirty = true; update(); @@ -219,110 +212,18 @@ void PenLayer::stamp(IRenderedTarget *target) if (!target || !m_fbo || !m_texture.isValid() || m_vao == 0 || m_vbo == 0) return; - const float stageWidth = m_engine->stageWidth() * m_scale; - const float stageHeight = m_engine->stageHeight() * m_scale; - - libscratchcpp::Rect bounds = target->getFastBounds(); - bounds.snapToInt(); - - if (!bounds.intersects(libscratchcpp::Rect(-stageWidth / 2, stageHeight / 2, stageWidth / 2, -stageHeight / 2))) - return; - - float x = 0; - float y = 0; - float angle = 180; - float scaleX = 1; - float scaleY = 1; - - SpriteModel *spriteModel = target->spriteModel(); - - if (spriteModel) { - libscratchcpp::Sprite *sprite = spriteModel->sprite(); - - switch (sprite->rotationStyle()) { - case libscratchcpp::Sprite::RotationStyle::AllAround: - angle = 270 - sprite->direction(); - break; + Q_ASSERT(m_fbo->isBound()); - case libscratchcpp::Sprite::RotationStyle::LeftRight: - scaleX = sgn(sprite->direction()); - break; - - default: - break; - } - - scaleY = sprite->size() / 100; - scaleX *= scaleY; - } - - scaleX *= m_scale; - scaleY *= m_scale; - - const Texture &texture = target->cpuTexture(); - - if (!texture.isValid()) - return; - - const float textureScale = texture.width() / static_cast(target->costumeWidth()); - const float skinWidth = texture.width(); - const float skinHeight = texture.height(); - - // Projection matrix - QMatrix4x4 projectionMatrix; - const float aspectRatio = skinHeight / skinWidth; - projectionMatrix.ortho(1.0f, -1.0f, aspectRatio, -aspectRatio, 0.1f, 0.0f); - projectionMatrix.scale(skinWidth / bounds.width() / m_scale, skinHeight / bounds.height() / m_scale); - - // Model matrix - // TODO: This should be calculated and cached by targets - QMatrix4x4 modelMatrix; - modelMatrix.rotate(angle, 0, 0, 1); - modelMatrix.scale(scaleX / textureScale, aspectRatio * scaleY / textureScale); m_glF->glDisable(GL_SCISSOR_TEST); m_glF->glDisable(GL_DEPTH_TEST); - m_glF->glEnable(GL_BLEND); - m_glF->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - // Create a FBO for the current texture - m_glF->glBindFramebuffer(GL_FRAMEBUFFER, m_stampFbo); - m_glF->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.handle(), 0); - - if (m_glF->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - qWarning() << "error: framebuffer incomplete (stamp " + target->scratchTarget()->name() + ")"; - m_glF->glBindFramebuffer(GL_FRAMEBUFFER, 0); - return; - } - - // Set viewport - m_glF->glViewport((stageWidth / 2) + bounds.left() * m_scale, (stageHeight / 2) + bounds.bottom() * m_scale, bounds.width() * m_scale, bounds.height() * m_scale); - - // Get the shader program for the current set of effects - ShaderManager *shaderManager = ShaderManager::instance(); - - const auto &effects = target->graphicEffects(); - QOpenGLShaderProgram *shaderProgram = shaderManager->getShaderProgram(effects); - Q_ASSERT(shaderProgram); - Q_ASSERT(shaderProgram->isLinked()); m_glF->glBindBuffer(GL_ARRAY_BUFFER, m_vbo); - - // Render to the target framebuffer - m_glF->glBindFramebuffer(GL_FRAMEBUFFER, m_fbo->handle()); - shaderProgram->bind(); m_glF->glBindVertexArray(m_vao); - m_glF->glActiveTexture(GL_TEXTURE0); - m_glF->glBindTexture(GL_TEXTURE_2D, texture.handle()); - shaderManager->setUniforms(shaderProgram, 0, texture.size(), effects); // set texture and effect uniforms - shaderProgram->setUniformValue("u_projectionMatrix", projectionMatrix); - shaderProgram->setUniformValue("u_modelMatrix", modelMatrix); - m_glF->glDrawArrays(GL_TRIANGLES, 0, 6); - - // Cleanup - shaderProgram->release(); + + target->render(m_scale); + m_glF->glBindVertexArray(0); m_glF->glBindBuffer(GL_ARRAY_BUFFER, 0); - m_glF->glBindFramebuffer(GL_FRAMEBUFFER, 0); m_glF->glEnable(GL_SCISSOR_TEST); m_glF->glEnable(GL_DEPTH_TEST); @@ -395,9 +296,18 @@ QRgb PenLayer::colorAtScratchPoint(double x, double y) const if ((x < 0 || x >= width) || (y < 0 || y >= height)) return qRgba(0, 0, 0, 0); + bool bound = m_fbo->isBound(); + + if (bound) + const_cast(this)->endFrame(); + GLubyte *data = m_textureManager.getTextureData(m_texture); const int index = (y * width + x) * 4; // RGBA channels Q_ASSERT(index >= 0 && index < width * height * 4); + + if (bound) + const_cast(this)->beginFrame(); + return qRgba(data[index], data[index + 1], data[index + 2], data[index + 3]); } @@ -420,8 +330,17 @@ const libscratchcpp::Rect &PenLayer::getBounds() const const double width = m_texture.width(); const double height = m_texture.height(); std::vector points; + + bool bound = m_fbo->isBound(); + + if (bound) + const_cast(this)->endFrame(); + m_textureManager.getTextureConvexHullPoints(m_texture, QSize(), ShaderManager::Effect::NoEffect, {}, points); + if (bound) + const_cast(this)->beginFrame(); + if (points.empty()) { m_bounds = libscratchcpp::Rect(); return m_bounds; @@ -468,6 +387,11 @@ void PenLayer::addPenLayer(libscratchcpp::IEngine *engine, IPenLayer *penLayer) m_projectPenLayers[engine] = penLayer; } +void PenLayer::removePenLayer(libscratchcpp::IEngine *engine) +{ + m_projectPenLayers.erase(engine); +} + QNanoQuickItemPainter *PenLayer::createItemPainter() const { m_glCtx = QOpenGLContext::currentContext(); diff --git a/src/penlayer.h b/src/penlayer.h index 53c614c..b5ada60 100644 --- a/src/penlayer.h +++ b/src/penlayer.h @@ -31,6 +31,9 @@ class PenLayer : public IPenLayer libscratchcpp::IEngine *engine() const override; void setEngine(libscratchcpp::IEngine *newEngine) override; + void beginFrame() override; + void endFrame() override; + bool hqPen() const; void setHqPen(bool newHqPen); @@ -47,7 +50,10 @@ class PenLayer : public IPenLayer const libscratchcpp::Rect &getBounds() const override; static IPenLayer *getProjectPenLayer(libscratchcpp::IEngine *engine); - static void addPenLayer(libscratchcpp::IEngine *engine, IPenLayer *penLayer); // for tests + + // For tests + static void addPenLayer(libscratchcpp::IEngine *engine, IPenLayer *penLayer); + static void removePenLayer(libscratchcpp::IEngine *engine); signals: void engineChanged(); @@ -61,7 +67,6 @@ class PenLayer : public IPenLayer void updateTexture(); static std::unordered_map m_projectPenLayers; - static inline GLuint m_stampFbo = 0; bool m_antialiasingEnabled = true; libscratchcpp::IEngine *m_engine = nullptr; bool m_hqPen = false; diff --git a/src/projectloader.cpp b/src/projectloader.cpp index 1453620..54e33de 100644 --- a/src/projectloader.cpp +++ b/src/projectloader.cpp @@ -12,7 +12,8 @@ #include "valuemonitormodel.h" #include "listmonitormodel.h" #include "renderedtarget.h" -// #include "blocks/penblocks.h" +#include "penlayer.h" +#include "blocks/penblocks.h" using namespace scratchcpprender; using namespace libscratchcpp; @@ -37,8 +38,8 @@ ProjectLoader::ProjectLoader(QObject *parent) : initTimer(); m_renderTimer.start(); - // TODO: Register pen blocks - // ScratchConfiguration::registerExtension(std::make_shared()); + // Register pen blocks + ScratchConfiguration::registerExtension(std::make_shared()); } ProjectLoader::~ProjectLoader() @@ -231,8 +232,16 @@ void ProjectLoader::timerEvent(QTimerEvent *event) m_unpositionedMonitors.clear(); + IPenLayer *penLayer = PenLayer::getProjectPenLayer(m_engine); + + if (penLayer) + penLayer->beginFrame(); + m_engine->step(); + if (penLayer) + penLayer->endFrame(); + if (m_running != m_engine->isRunning()) { m_running = !m_running; emit runningChanged(); diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index b2130f6..a29b77e 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -23,6 +23,13 @@ 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 +// TODO: Move this to a separate class +template +short sgn(T x) +{ + return (T(0) < x) - (x < T(0)); +} + RenderedTarget::RenderedTarget(QQuickItem *parent) : IRenderedTarget(parent) { @@ -507,6 +514,62 @@ void RenderedTarget::mouseMoveEvent(QMouseEvent *event) } } +void RenderedTarget::render(double scale) const +{ + if (!m_glF) { + m_glF = std::make_unique(); + m_glF->initializeOpenGLFunctions(); + } + + if (!m_cpuTexture.isValid()) + return; + + const float stageWidth = m_engine->stageWidth() * scale; + const float stageHeight = m_engine->stageHeight() * scale; + + libscratchcpp::Rect bounds = getFastBounds(); + bounds.snapToInt(); + + if (!bounds.intersects(libscratchcpp::Rect(-stageWidth / 2, stageHeight / 2, stageWidth / 2, -stageHeight / 2))) + return; + + QMatrix4x4 modelMatrix, projectionMatrix; + getMatrices(modelMatrix, projectionMatrix); + modelMatrix.scale(scale); + projectionMatrix.scale(1.0f / scale); + + m_glF->glEnable(GL_BLEND); + m_glF->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + m_glF->glViewport((stageWidth / 2) + bounds.left() * scale, (stageHeight / 2) + bounds.bottom() * scale, bounds.width() * scale, bounds.height() * scale); + + ShaderManager *shaderManager = ShaderManager::instance(); + + if (!m_shaderProgram) { + m_shaderProgram = shaderManager->getShaderProgram(this, m_graphicEffects); + Q_ASSERT(m_shaderProgram); + Q_ASSERT(m_shaderProgram->isLinked()); + + m_shaderProgram->bind(); + ShaderManager::setUniforms(m_shaderProgram, 0, m_cpuTexture.size(), m_graphicEffects); + } + + GLint currentProgram = 0; + m_glF->glGetIntegerv(GL_CURRENT_PROGRAM, ¤tProgram); + + if (static_cast(currentProgram) != m_shaderProgram->programId()) + m_shaderProgram->bind(); + + m_glF->glActiveTexture(GL_TEXTURE0); + m_glF->glBindTexture(GL_TEXTURE_2D, m_cpuTexture.handle()); + + m_shaderProgram->setUniformValue("u_projectionMatrix", projectionMatrix); + m_shaderProgram->setUniformValue("u_modelMatrix", modelMatrix); + m_glF->glDrawArrays(GL_TRIANGLES, 0, 6); + + // NOTE: Keep the shader program bound for future use +} + Texture RenderedTarget::texture() const { return m_texture; @@ -561,6 +624,7 @@ void RenderedTarget::setGraphicEffect(ShaderManager::Effect effect, double value if (changed) { update(); + m_shaderProgram = nullptr; if (ShaderManager::effectShapeChanges(effect)) { m_convexHullDirty = true; @@ -584,6 +648,7 @@ void RenderedTarget::clearGraphicEffects() m_graphicEffects.clear(); m_graphicEffectMask = ShaderManager::Effect::NoEffect; + m_shaderProgram = nullptr; } const std::vector &RenderedTarget::hullPoints() const @@ -699,16 +764,19 @@ void RenderedTarget::calculatePos() setTransformOrigin(QQuickItem::Center); m_transformedHullDirty = true; + m_matricesDirty = true; } void RenderedTarget::calculateRotation() { // Direction bool oldMirrorHorizontally = m_mirrorHorizontally; + m_renderAngle = 180.0f; switch (m_rotationStyle) { case Sprite::RotationStyle::AllAround: setRotation(m_direction - 90); + m_renderAngle = 270.0f - m_direction; m_mirrorHorizontally = (false); break; @@ -730,6 +798,7 @@ void RenderedTarget::calculateRotation() emit mirrorHorizontallyChanged(); m_transformedHullDirty = true; + m_matricesDirty = true; } void RenderedTarget::calculateSize() @@ -747,6 +816,8 @@ void RenderedTarget::calculateSize() m_convexHullDirty = true; m_transformedHullDirty = true; + m_matricesDirty = true; + m_shaderProgram = nullptr; } } @@ -1115,6 +1186,35 @@ QRgb RenderedTarget::sampleColor3b(double x, double y, const std::vector(costumeWidth()); + const float aspectRatio = height / width; + + libscratchcpp::Rect bounds = getFastBounds(); + bounds.snapToInt(); + + m_projectionMatrix = QMatrix4x4(); + m_projectionMatrix.ortho(1.0f, -1.0f, aspectRatio, -aspectRatio, 0.1f, 0.0f); + m_projectionMatrix.scale(width / bounds.width(), height / bounds.height()); + + m_modelMatrix = QMatrix4x4(); + m_modelMatrix.rotate(m_renderAngle, 0, 0, 1); + m_modelMatrix.scale(scaleX / textureScale, aspectRatio * scaleY / textureScale); + + m_matricesDirty = false; + } + + modelMatrix = m_modelMatrix; + projectionMatrix = m_projectionMatrix; +} + bool RenderedTarget::mirrorHorizontally() const { return m_mirrorHorizontally; diff --git a/src/renderedtarget.h b/src/renderedtarget.h index 5a94bb2..76e1c18 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -85,6 +85,8 @@ class RenderedTarget : public IRenderedTarget bool mirrorHorizontally() const override; + void render(double scale) const override; + Texture texture() const override; const Texture &cpuTexture() const override; int costumeWidth() const override; @@ -143,6 +145,8 @@ class RenderedTarget : public IRenderedTarget static bool maskMatches(QRgb a, QRgb b); QRgb sampleColor3b(double x, double y, const std::vector &targets) const; + void getMatrices(QMatrix4x4 &modelMatrix, QMatrix4x4 &projectionMatrix) const; + libscratchcpp::IEngine *m_engine = nullptr; libscratchcpp::Costume *m_costume = nullptr; StageModel *m_stageModel = nullptr; @@ -157,15 +161,17 @@ class RenderedTarget : public IRenderedTarget Texture m_oldTexture; Texture m_cpuTexture; // without stage scale mutable std::shared_ptr m_textureManager; // NOTE: Use textureManager()! - std::unique_ptr m_glF; + mutable std::unique_ptr m_glF; mutable std::unordered_map m_graphicEffects; mutable ShaderManager::Effect m_graphicEffectMask = ShaderManager::Effect::NoEffect; + mutable QOpenGLShaderProgram *m_shaderProgram = nullptr; double m_size = 1; double m_x = 0; double m_y = 0; double m_width = 1; double m_height = 1; double m_direction = 90; + float m_renderAngle = 180.0f; libscratchcpp::Sprite::RotationStyle m_rotationStyle = libscratchcpp::Sprite::RotationStyle::AllAround; bool m_mirrorHorizontally = false; double m_stageScale = 1; @@ -175,7 +181,10 @@ class RenderedTarget : public IRenderedTarget std::vector m_hullPoints; mutable bool m_transformedHullDirty = true; mutable std::vector m_transformedHullPoints; // NOTE: Use transformedHullPoints(); - bool m_clicked = false; // left mouse button only! + mutable bool m_matricesDirty = false; + mutable QMatrix4x4 m_modelMatrix; // NOTE: Use getMatrices()! + mutable QMatrix4x4 m_projectionMatrix; // NOTE: Use getMatrices()! + bool m_clicked = false; // left mouse button only! double m_dragX = 0; double m_dragY = 0; double m_dragDeltaX = 0; diff --git a/src/shadermanager.cpp b/src/shadermanager.cpp index 93625ba..3c78798 100644 --- a/src/shadermanager.cpp +++ b/src/shadermanager.cpp @@ -97,7 +97,7 @@ ShaderManager *ShaderManager::instance() return globalInstance; } -QOpenGLShaderProgram *ShaderManager::getShaderProgram(const std::unordered_map &effectValues) +QOpenGLShaderProgram *ShaderManager::getShaderProgram(const IRenderedTarget *target, const std::unordered_map &effectValues) { int effectBits = 0; bool firstSet = false; @@ -115,11 +115,24 @@ QOpenGLShaderProgram *ShaderManager::getShaderProgram(const std::unordered_mapsecond; + } else { + const auto &map = it->second; + auto it = map.find(target); + + if (it == map.cend()) { + // Create a new shader program if this combination doesn't exist for the given target + QOpenGLShaderProgram *program = createShaderProgram(effectValues); + + if (program) + m_shaderPrograms[effectBits][target] = program; + + return program; + } else + return it->second; + } } void ShaderManager::getUniformValuesForEffects(const std::unordered_map &effectValues, std::unordered_map &dst) diff --git a/src/shadermanager.h b/src/shadermanager.h index 210ee98..77323fd 100644 --- a/src/shadermanager.h +++ b/src/shadermanager.h @@ -12,6 +12,8 @@ class QOpenGLShader; namespace scratchcpprender { +class IRenderedTarget; + class ShaderManager : public QObject { public: @@ -31,9 +33,9 @@ class ShaderManager : public QObject static ShaderManager *instance(); - QOpenGLShaderProgram *getShaderProgram(const std::unordered_map &effectValues); + QOpenGLShaderProgram *getShaderProgram(const IRenderedTarget *target, const std::unordered_map &effectValues); static void getUniformValuesForEffects(const std::unordered_map &effectValues, std::unordered_map &dst); - void setUniforms(QOpenGLShaderProgram *program, int textureUnit, const QSize skinSize, const std::unordered_map &effectValues); + static void setUniforms(QOpenGLShaderProgram *program, int textureUnit, const QSize skinSize, const std::unordered_map &effectValues); static const std::unordered_set &effects(); static bool effectShapeChanges(Effect effect); @@ -52,7 +54,7 @@ class ShaderManager : public QObject static std::unordered_set m_effects; QOpenGLShader *m_vertexShader = nullptr; - std::unordered_map m_shaderPrograms; + std::unordered_map> m_shaderPrograms; QByteArray m_fragmentShaderSource; }; diff --git a/src/targetpainter.cpp b/src/targetpainter.cpp index 7eb2dd7..64df358 100644 --- a/src/targetpainter.cpp +++ b/src/targetpainter.cpp @@ -62,7 +62,7 @@ void TargetPainter::paint(QNanoPainter *painter) ShaderManager *shaderManager = ShaderManager::instance(); const auto &effects = m_target->graphicEffects(); - QOpenGLShaderProgram *shaderProgram = shaderManager->getShaderProgram(effects); + QOpenGLShaderProgram *shaderProgram = shaderManager->getShaderProgram(m_target, effects); Q_ASSERT(shaderProgram); Q_ASSERT(shaderProgram->isLinked()); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f697644..0159df6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -36,6 +36,7 @@ add_subdirectory(penattributes) add_subdirectory(penstate) add_subdirectory(penlayer) add_subdirectory(penlayerpainter) +add_subdirectory(blocks) add_subdirectory(graphicseffect) add_subdirectory(shadermanager) add_subdirectory(textbubbleshape) diff --git a/test/blocks/CMakeLists.txt b/test/blocks/CMakeLists.txt new file mode 100644 index 0000000..32c005f --- /dev/null +++ b/test/blocks/CMakeLists.txt @@ -0,0 +1,33 @@ +add_library( + block_test_deps SHARED + util.cpp + util.h +) + +target_link_libraries( + block_test_deps + GTest::gtest_main + scratchcpp + scratchcpp-render +) + +# 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 + scratchcpp-render + scratchcpprender_mocks + qnanopainter + block_test_deps + ${QT_LIBS} +) + +add_test(pen_blocks_test) +gtest_discover_tests(pen_blocks_test) diff --git a/test/blocks/pen_blocks_test.cpp b/test/blocks/pen_blocks_test.cpp new file mode 100644 index 0000000..13bd5fa --- /dev/null +++ b/test/blocks/pen_blocks_test.cpp @@ -0,0 +1,2854 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "penlayer.h" +#include "spritemodel.h" +#include "stagemodel.h" +#include "renderedtarget.h" +#include "blocks/penblocks.h" +#include "util.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; +using namespace libscratchcpp::test; + +using ::testing::Return; +using ::testing::ReturnRef; + +class PenBlocksTest : public testing::Test +{ + public: + void SetUp() override + { + m_extension = std::make_unique(); + m_engine = m_project.engine().get(); + m_extension->registerBlocks(m_engine); + registerBlocks(m_engine, m_extension.get()); + + PenLayer::addPenLayer(&m_engineMock, &m_penLayer); + + EXPECT_CALL(m_engineMock, targets()).WillRepeatedly(ReturnRef(m_engine->targets())); + } + + void TearDown() override { PenLayer::removePenLayer(&m_engineMock); } + + std::shared_ptr buildScript(ScriptBuilder &builder, Target *target) + { + auto block = builder.currentBlock(); + + m_compiler = std::make_unique(&m_engineMock, target); + auto code = m_compiler->compile(block); + m_script = std::make_unique