diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 946a1f7..5a38410 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -55,6 +55,7 @@ qt_add_qml_module(scratchcpp-render penlayerpainter.cpp penlayerpainter.h penattributes.h + penstate.h blocks/penextension.cpp blocks/penextension.h blocks/penblocks.cpp diff --git a/src/blocks/penblocks.cpp b/src/blocks/penblocks.cpp index e314b7d..88c38c4 100644 --- a/src/blocks/penblocks.cpp +++ b/src/blocks/penblocks.cpp @@ -2,14 +2,26 @@ #include #include +#include #include "penblocks.h" #include "penlayer.h" +#include "penstate.h" #include "spritemodel.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; + +const std::unordered_map + PenBlocks::COLOR_PARAM_MAP = { { "color", ColorParam::COLOR }, { "saturation", ColorParam::SATURATION }, { "brightness", ColorParam::BRIGHTNESS }, { "transparency", ColorParam::TRANSPARENCY } }; + std::string PenBlocks::name() const { return "Pen"; @@ -21,6 +33,23 @@ void PenBlocks::registerBlocks(IEngine *engine) engine->addCompileFunction(this, "pen_clear", &compileClear); 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); + + // Inputs + engine->addInput(this, "COLOR", COLOR); + engine->addInput(this, "COLOR_PARAM", COLOR_PARAM); + engine->addInput(this, "VALUE", VALUE); + engine->addInput(this, "SIZE", SIZE); + engine->addInput(this, "SHADE", SHADE); + engine->addInput(this, "HUE", HUE); } void PenBlocks::compileClear(Compiler *compiler) @@ -38,6 +67,106 @@ void PenBlocks::compilePenUp(Compiler *compiler) compiler->addFunctionCall(&penUp); } +void PenBlocks::compileSetPenColorToColor(libscratchcpp::Compiler *compiler) +{ + compiler->addInput(COLOR); + compiler->addFunctionCall(&setPenColorToColor); +} + +void PenBlocks::compileChangePenColorParamBy(libscratchcpp::Compiler *compiler) +{ + Input *input = compiler->input(COLOR_PARAM); + + if (input->type() != Input::Type::ObscuredShadow) { + assert(input->pointsToDropdownMenu()); + std::string value = input->selectedMenuItem(); + BlockFunc f = nullptr; + + if (value == "color") + f = &changePenColorBy; + else if (value == "saturation") + f = &changePenSaturationBy; + else if (value == "brightness") + f = &changePenBrightnessBy; + else if (value == "transparency") + f = &changePenTransparencyBy; + + if (f) { + compiler->addInput(VALUE); + compiler->addFunctionCall(f); + } + } else { + compiler->addInput(input); + compiler->addInput(VALUE); + compiler->addFunctionCall(&changePenColorParamBy); + } +} + +void PenBlocks::compileSetPenColorParamTo(Compiler *compiler) +{ + Input *input = compiler->input(COLOR_PARAM); + + if (input->type() != Input::Type::ObscuredShadow) { + assert(input->pointsToDropdownMenu()); + std::string value = input->selectedMenuItem(); + BlockFunc f = nullptr; + + if (value == "color") + f = &setPenColorTo; + else if (value == "saturation") + f = &setPenSaturationTo; + else if (value == "brightness") + f = &setPenBrightnessTo; + else if (value == "transparency") + f = &setPenTransparencyTo; + + if (f) { + compiler->addInput(VALUE); + compiler->addFunctionCall(f); + } + } else { + compiler->addInput(input); + compiler->addInput(VALUE); + compiler->addFunctionCall(&setPenColorParamTo); + } +} + +void PenBlocks::compileChangePenSizeBy(libscratchcpp::Compiler *compiler) +{ + compiler->addInput(SIZE); + compiler->addFunctionCall(&changePenSizeBy); +} + +void PenBlocks::compileSetPenSizeTo(libscratchcpp::Compiler *compiler) +{ + compiler->addInput(SIZE); + compiler->addFunctionCall(&setPenSizeTo); +} + +void PenBlocks::compileChangePenShadeBy(Compiler *compiler) +{ + compiler->addInput(SHADE); + compiler->addFunctionCall(&changePenShadeBy); +} + +void PenBlocks::compileSetPenShadeToNumber(libscratchcpp::Compiler *compiler) +{ + compiler->addInput(SHADE); + compiler->addFunctionCall(&setPenShadeToNumber); +} + +void PenBlocks::compileChangePenHueBy(libscratchcpp::Compiler *compiler) +{ + compiler->addInput(HUE); + compiler->addFunctionCall(&changePenHueBy); +} + +void PenBlocks::compileSetPenHueToNumber(libscratchcpp::Compiler *compiler) +{ + compiler->addInput(HUE); + compiler->addFunctionCall(&setPenHueToNumber); +} + unsigned int PenBlocks::clear(VirtualMachine *vm) { IPenLayer *penLayer = PenLayer::getProjectPenLayer(vm->engine()); @@ -52,13 +181,7 @@ unsigned int PenBlocks::clear(VirtualMachine *vm) 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()); + SpriteModel *model = getSpriteModel(vm); if (model) model->setPenDown(true); @@ -67,17 +190,340 @@ unsigned int PenBlocks::penDown(VirtualMachine *vm) } unsigned int PenBlocks::penUp(libscratchcpp::VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) + model->setPenDown(false); + + return 0; +} + +unsigned int PenBlocks::changePenSizeBy(libscratchcpp::VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) + model->penAttributes().diameter = std::clamp(model->penAttributes().diameter + vm->getInput(0, 1)->toDouble(), PEN_SIZE_MIN, PEN_SIZE_MAX); + + return 1; +} + +unsigned int PenBlocks::setPenSizeTo(libscratchcpp::VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) + model->penAttributes().diameter = std::clamp(vm->getInput(0, 1)->toDouble(), PEN_SIZE_MIN, PEN_SIZE_MAX); + + return 1; +} + +unsigned int PenBlocks::changePenShadeBy(libscratchcpp::VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) { + PenState &penState = model->penState(); + setPenShade(penState.shade + vm->getInput(0, 1)->toDouble(), penState); + } + + return 1; +} + +unsigned int PenBlocks::setPenShadeToNumber(libscratchcpp::VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) + setPenShade(vm->getInput(0, 1)->toInt(), model->penState()); + + return 1; +} + +unsigned int PenBlocks::changePenHueBy(libscratchcpp::VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) { + PenState &penState = model->penState(); + const double colorChange = vm->getInput(0, 1)->toDouble() / 2; + setOrChangeColorParam(ColorParam::COLOR, colorChange, penState, true); + legacyUpdatePenColor(penState); + } + + return 1; +} + +unsigned int PenBlocks::setPenHueToNumber(libscratchcpp::VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) { + PenState &penState = model->penState(); + const double colorValue = vm->getInput(0, 1)->toDouble() / 2; + setOrChangeColorParam(ColorParam::COLOR, colorValue, penState, false); + penState.transparency = 0; + legacyUpdatePenColor(penState); + } + + return 1; +} + +unsigned int PenBlocks::setPenColorToColor(libscratchcpp::VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) { + const Value *value = vm->getInput(0, 1); + std::string stringValue; + PenState &penState = model->penState(); + QColor newColor; + + if (value->isString()) + stringValue = value->toString(); + + if (!stringValue.empty() && stringValue[0] == '#') { + bool valid = false; + + if (stringValue.size() <= 7) // #RRGGBB + { + newColor = QColor::fromString(stringValue); + valid = newColor.isValid(); + } + + if (!valid) + newColor = Qt::black; + + } else + newColor = QColor::fromRgba(static_cast(value->toLong())); + + QColor hsv = newColor.toHsv(); + penState.color = (hsv.hue() / 360.0) * 100; + penState.saturation = hsv.saturationF() * 100; + penState.brightness = hsv.valueF() * 100; + + if (newColor.alpha() > 0) + penState.transparency = 100 * (1 - newColor.alpha() / 255.0); + else + penState.transparency = 0; + + penState.updateColor(); + + // Set the legacy "shade" value the same way Scratch 2 did. + penState.shade = penState.brightness / 2; + } + + return 1; +} + +unsigned int PenBlocks::changePenColorParamBy(VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) { + const auto it = COLOR_PARAM_MAP.find(vm->getInput(0, 2)->toString()); + + if (it == COLOR_PARAM_MAP.cend()) + return 2; + + setOrChangeColorParam(it->second, vm->getInput(1, 2)->toDouble(), model->penState(), true); + } + + return 2; +} + +unsigned int PenBlocks::changePenColorBy(VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) + setOrChangeColorParam(ColorParam::COLOR, vm->getInput(0, 1)->toDouble(), model->penState(), true); + + return 1; +} + +unsigned int PenBlocks::changePenSaturationBy(VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) + setOrChangeColorParam(ColorParam::SATURATION, vm->getInput(0, 1)->toDouble(), model->penState(), true); + + return 1; +} + +unsigned int PenBlocks::changePenBrightnessBy(VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) + setOrChangeColorParam(ColorParam::BRIGHTNESS, vm->getInput(0, 1)->toDouble(), model->penState(), true); + + return 1; +} + +unsigned int PenBlocks::changePenTransparencyBy(VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) + setOrChangeColorParam(ColorParam::TRANSPARENCY, vm->getInput(0, 1)->toDouble(), model->penState(), true); + + return 1; +} + +unsigned int PenBlocks::setPenColorParamTo(VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) { + const auto it = COLOR_PARAM_MAP.find(vm->getInput(0, 2)->toString()); + + if (it == COLOR_PARAM_MAP.cend()) + return 2; + + setOrChangeColorParam(it->second, vm->getInput(1, 2)->toDouble(), model->penState(), false); + } + + return 2; +} + +unsigned int PenBlocks::setPenColorTo(VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) + setOrChangeColorParam(ColorParam::COLOR, vm->getInput(0, 1)->toDouble(), model->penState(), false); + + return 1; +} + +unsigned int PenBlocks::setPenSaturationTo(VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) + setOrChangeColorParam(ColorParam::SATURATION, vm->getInput(0, 1)->toDouble(), model->penState(), false); + + return 1; +} + +unsigned int PenBlocks::setPenBrightnessTo(VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) + setOrChangeColorParam(ColorParam::BRIGHTNESS, vm->getInput(0, 1)->toDouble(), model->penState(), false); + + return 1; +} + +unsigned int PenBlocks::setPenTransparencyTo(VirtualMachine *vm) +{ + SpriteModel *model = getSpriteModel(vm); + + if (model) + setOrChangeColorParam(ColorParam::TRANSPARENCY, vm->getInput(0, 1)->toDouble(), model->penState(), false); + + return 1; +} + +SpriteModel *PenBlocks::getSpriteModel(libscratchcpp::VirtualMachine *vm) { Target *target = vm->target(); if (!target || target->isStage()) - return 0; + return nullptr; Sprite *sprite = static_cast(target); SpriteModel *model = static_cast(sprite->getInterface()); + return model; +} - if (model) - model->setPenDown(false); +void PenBlocks::setOrChangeColorParam(ColorParam param, double value, PenState &penState, bool change) +{ + switch (param) { + case ColorParam::COLOR: + penState.color = wrapClamp(value + (change ? penState.color : 0), 0, 100); + break; - return 0; + case ColorParam::SATURATION: + penState.saturation = std::clamp(value + (change ? penState.saturation : 0), COLOR_PARAM_MIN, COLOR_PARAM_MAX); + break; + + case ColorParam::BRIGHTNESS: + penState.brightness = std::clamp(value + (change ? penState.brightness : 0), COLOR_PARAM_MIN, COLOR_PARAM_MAX); + break; + + case ColorParam::TRANSPARENCY: + penState.transparency = std::clamp(value + (change ? penState.transparency : 0), COLOR_PARAM_MIN, COLOR_PARAM_MAX); + break; + + default: + assert(false); + return; + } + + penState.updateColor(); +} + +void PenBlocks::setPenShade(int shade, PenState &penState) +{ + // https://github.com/scratchfoundation/scratch-vm/blob/8dbcc1fc8f8d8c4f1e40629fe8a388149d6dfd1c/src/extensions/scratch3_pen/index.js#L718-L730 + // Wrap clamp the new shade value the way Scratch 2 did + shade = shade % 200; + + if (shade < 0) + shade += 200; + + // And store the shade that was used to compute this new color for later use + penState.shade = shade; + + legacyUpdatePenColor(penState); +} + +void PenBlocks::legacyUpdatePenColor(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 = mixRgb(0, rgb, (10 + shade) / 60); + else + rgb = mixRgb(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(); +} + +double PenBlocks::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); +} + +QRgb PenBlocks::mixRgb(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); } diff --git a/src/blocks/penblocks.h b/src/blocks/penblocks.h index 7da4ba1..5eb057f 100644 --- a/src/blocks/penblocks.h +++ b/src/blocks/penblocks.h @@ -2,16 +2,26 @@ #pragma once +#include #include namespace scratchcpprender { +class SpriteModel; +class PenState; + class PenBlocks : public libscratchcpp::IBlockSection { public: enum Inputs { + COLOR, + COLOR_PARAM, + VALUE, + SIZE, + SHADE, + HUE }; std::string name() const override; @@ -21,10 +31,57 @@ class PenBlocks : public libscratchcpp::IBlockSection static void compileClear(libscratchcpp::Compiler *compiler); static void compilePenDown(libscratchcpp::Compiler *compiler); static void compilePenUp(libscratchcpp::Compiler *compiler); + static void compileSetPenColorToColor(libscratchcpp::Compiler *compiler); + static void compileChangePenColorParamBy(libscratchcpp::Compiler *compiler); + static void compileSetPenColorParamTo(libscratchcpp::Compiler *compiler); + static void compileChangePenSizeBy(libscratchcpp::Compiler *compiler); + static void compileSetPenSizeTo(libscratchcpp::Compiler *compiler); + static void compileChangePenShadeBy(libscratchcpp::Compiler *compiler); + static void compileSetPenShadeToNumber(libscratchcpp::Compiler *compiler); + static void compileChangePenHueBy(libscratchcpp::Compiler *compiler); + static void compileSetPenHueToNumber(libscratchcpp::Compiler *compiler); static unsigned int clear(libscratchcpp::VirtualMachine *vm); static unsigned int penDown(libscratchcpp::VirtualMachine *vm); static unsigned int penUp(libscratchcpp::VirtualMachine *vm); + static unsigned int setPenColorToColor(libscratchcpp::VirtualMachine *vm); + + static unsigned int changePenColorParamBy(libscratchcpp::VirtualMachine *vm); + static unsigned int changePenColorBy(libscratchcpp::VirtualMachine *vm); + static unsigned int changePenSaturationBy(libscratchcpp::VirtualMachine *vm); + static unsigned int changePenBrightnessBy(libscratchcpp::VirtualMachine *vm); + static unsigned int changePenTransparencyBy(libscratchcpp::VirtualMachine *vm); + + static unsigned int setPenColorParamTo(libscratchcpp::VirtualMachine *vm); + static unsigned int setPenColorTo(libscratchcpp::VirtualMachine *vm); + static unsigned int setPenSaturationTo(libscratchcpp::VirtualMachine *vm); + static unsigned int setPenBrightnessTo(libscratchcpp::VirtualMachine *vm); + static unsigned int setPenTransparencyTo(libscratchcpp::VirtualMachine *vm); + + static unsigned int changePenSizeBy(libscratchcpp::VirtualMachine *vm); + static unsigned int setPenSizeTo(libscratchcpp::VirtualMachine *vm); + static unsigned int changePenShadeBy(libscratchcpp::VirtualMachine *vm); + static unsigned int setPenShadeToNumber(libscratchcpp::VirtualMachine *vm); + static unsigned int changePenHueBy(libscratchcpp::VirtualMachine *vm); + static unsigned int setPenHueToNumber(libscratchcpp::VirtualMachine *vm); + + private: + enum class ColorParam + { + COLOR, + SATURATION, + BRIGHTNESS, + TRANSPARENCY + }; + + static SpriteModel *getSpriteModel(libscratchcpp::VirtualMachine *vm); + static void setOrChangeColorParam(ColorParam param, double value, PenState &penState, bool change); + static void setPenShade(int shade, PenState &penState); + static void legacyUpdatePenColor(PenState &penState); + static double wrapClamp(double n, double min, double max); + static QRgb mixRgb(QRgb rgb0, QRgb rgb1, double fraction1); + + static const std::unordered_map COLOR_PARAM_MAP; }; } // namespace scratchcpprender diff --git a/src/penstate.h b/src/penstate.h new file mode 100644 index 0000000..aa134f2 --- /dev/null +++ b/src/penstate.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +#include "penattributes.h" + +namespace scratchcpprender +{ + +struct PenState +{ + bool penDown = false; + double color = 66.66; + double saturation = 100; + double brightness = 100; + double transparency = 0; + double shade = 50; // for legacy blocks + PenAttributes penAttributes; + + void updateColor() + { + int h = color * 360 / 100; + h %= 360; + + if (h < 0) + h += 360; + + const int s = saturation * 255 / 100; + const int v = brightness * 255 / 100; + const int a = 255 - transparency * 255 / 100; + + penAttributes.color = QColor::fromHsv(h, s, v, a); + } +}; + +} // namespace scratchcpprender diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp index a443f99..8a5880f 100644 --- a/src/spritemodel.cpp +++ b/src/spritemodel.cpp @@ -34,8 +34,7 @@ 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; + cloneModel->m_penState = m_penState; clone->setInterface(cloneModel); emit cloned(cloneModel); } @@ -66,8 +65,8 @@ void SpriteModel::onYChanged(double 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); + if (m_penState.penDown && m_penLayer) { + m_penLayer->drawLine(m_penState.penAttributes, oldX, oldY, newX, newY); libscratchcpp::IEngine *engine = m_sprite->engine(); if (engine) @@ -145,22 +144,27 @@ void SpriteModel::setPenLayer(IPenLayer *newPenLayer) emit penLayerChanged(); } +PenState &SpriteModel::penState() +{ + return m_penState; +} + PenAttributes &SpriteModel::penAttributes() { - return m_penAttributes; + return m_penState.penAttributes; } bool SpriteModel::penDown() const { - return m_penDown; + return m_penState.penDown; } void SpriteModel::setPenDown(bool newPenDown) { - m_penDown = newPenDown; + m_penState.penDown = newPenDown; - if (m_penDown && m_penLayer && m_sprite) { - m_penLayer->drawPoint(m_penAttributes, m_sprite->x(), m_sprite->y()); + if (m_penState.penDown && m_penLayer && m_sprite) { + m_penLayer->drawPoint(m_penState.penAttributes, m_sprite->x(), m_sprite->y()); libscratchcpp::IEngine *engine = m_sprite->engine(); if (engine) diff --git a/src/spritemodel.h b/src/spritemodel.h index 64473f0..c289d85 100644 --- a/src/spritemodel.h +++ b/src/spritemodel.h @@ -6,7 +6,7 @@ #include #include -#include "penattributes.h" +#include "penstate.h" Q_MOC_INCLUDE("renderedtarget.h"); Q_MOC_INCLUDE("ipenlayer.h"); @@ -58,6 +58,7 @@ class SpriteModel IPenLayer *penLayer() const; void setPenLayer(IPenLayer *newPenLayer); + PenState &penState(); PenAttributes &penAttributes(); bool penDown() const; @@ -75,8 +76,7 @@ class SpriteModel libscratchcpp::Sprite *m_sprite = nullptr; IRenderedTarget *m_renderedTarget = nullptr; IPenLayer *m_penLayer = nullptr; - PenAttributes m_penAttributes; - bool m_penDown = false; + PenState m_penState; SpriteModel *m_cloneRoot = nullptr; }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 85eaac7..4a98961 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -32,6 +32,7 @@ add_subdirectory(monitor_models) add_subdirectory(texture) add_subdirectory(skins) add_subdirectory(penattributes) +add_subdirectory(penstate) add_subdirectory(penlayer) add_subdirectory(penlayerpainter) add_subdirectory(blocks) diff --git a/test/blocks/pen_blocks_test.cpp b/test/blocks/pen_blocks_test.cpp index e6d9f66..57b423e 100644 --- a/test/blocks/pen_blocks_test.cpp +++ b/test/blocks/pen_blocks_test.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,52 @@ class PenBlocksTest : public testing::Test block->addInput(input); } + void addObscuredInput(std::shared_ptr block, const std::string &name, PenBlocks::Inputs id, std::shared_ptr valueBlock) const + { + auto input = std::make_shared(name, Input::Type::ObscuredShadow); + input->setValueBlock(valueBlock); + input->setInputId(id); + block->addInput(input); + } + + std::shared_ptr addNullInput(std::shared_ptr block, const std::string &name, PenBlocks::Inputs id) const + { + auto input = std::make_shared(name, Input::Type::Shadow); + input->setInputId(id); + block->addInput(input); + + return input; + } + + void addDropdownInput(std::shared_ptr block, const std::string &name, PenBlocks::Inputs id, const std::string &selectedValue, std::shared_ptr valueBlock = nullptr) const + { + if (valueBlock) + addObscuredInput(block, name, id, valueBlock); + else { + auto input = addNullInput(block, name, id); + auto menu = std::make_shared(block->id() + "_menu", block->opcode() + "_menu"); + input->setValueBlock(menu); + addDropdownField(menu, name, -1, selectedValue, -1); + } + } + + void addDropdownField(std::shared_ptr block, const std::string &name, int id, const std::string &value, int valueId) const + { + auto field = std::make_shared(name, value); + field->setFieldId(id); + field->setSpecialValueId(valueId); + block->addField(field); + } + + std::shared_ptr createNullBlock(const std::string &id) + { + std::shared_ptr block = std::make_shared(id, ""); + BlockComp func = [](Compiler *compiler) { compiler->addInstruction(vm::OP_NULL); }; + block->setCompileFunction(func); + + return block; + } + std::unique_ptr m_section; EngineMock m_engineMock; }; @@ -47,6 +94,23 @@ TEST_F(PenBlocksTest, RegisterBlocks) 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)); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_setPenColorToColor", &PenBlocks::compileSetPenColorToColor)); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_changePenColorParamBy", &PenBlocks::compileChangePenColorParamBy)); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_setPenColorParamTo", &PenBlocks::compileSetPenColorParamTo)); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_changePenSizeBy", &PenBlocks::compileChangePenSizeBy)); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_setPenSizeTo", &PenBlocks::compileSetPenSizeTo)); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_changePenShadeBy", &PenBlocks::compileChangePenShadeBy)); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_setPenShadeToNumber", &PenBlocks::compileSetPenShadeToNumber)); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_changePenHueBy", &PenBlocks::compileChangePenHueBy)); + EXPECT_CALL(m_engineMock, addCompileFunction(m_section.get(), "pen_setPenHueToNumber", &PenBlocks::compileSetPenHueToNumber)); + + // Inputs + EXPECT_CALL(m_engineMock, addInput(m_section.get(), "COLOR", PenBlocks::COLOR)); + EXPECT_CALL(m_engineMock, addInput(m_section.get(), "COLOR_PARAM", PenBlocks::COLOR_PARAM)); + EXPECT_CALL(m_engineMock, addInput(m_section.get(), "VALUE", PenBlocks::VALUE)); + EXPECT_CALL(m_engineMock, addInput(m_section.get(), "SIZE", PenBlocks::SIZE)); + EXPECT_CALL(m_engineMock, addInput(m_section.get(), "SHADE", PenBlocks::SHADE)); + EXPECT_CALL(m_engineMock, addInput(m_section.get(), "HUE", PenBlocks::HUE)); m_section->registerBlocks(&m_engineMock); } @@ -170,3 +234,1055 @@ TEST_F(PenBlocksTest, PenUpImpl) ASSERT_EQ(vm.registerCount(), 0); ASSERT_FALSE(model.penDown()); } + +TEST_F(PenBlocksTest, SetPenColorToColor) +{ + Compiler compiler(&m_engineMock); + + // set pen color to ("#AABBCC") + auto block1 = std::make_shared("a", "pen_setPenColorToColor"); + addValueInput(block1, "COLOR", PenBlocks::COLOR, "#AABBCC"); + + // set pen color to (null block) + auto block2 = std::make_shared("b", "pen_setPenColorToColor"); + addObscuredInput(block2, "COLOR", PenBlocks::COLOR, createNullBlock("c")); + + compiler.init(); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenColorToColor)).WillOnce(Return(2)); + compiler.setBlock(block1); + PenBlocks::compileSetPenColorToColor(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenColorToColor)).WillOnce(Return(2)); + compiler.setBlock(block2); + PenBlocks::compileSetPenColorToColor(&compiler); + + compiler.end(); + + ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); + ASSERT_EQ(compiler.constValues().size(), 1); + ASSERT_EQ(compiler.constValues()[0].toString(), "#AABBCC"); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(PenBlocksTest, SetPenColorToColorImpl) +{ + static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode4[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode5[] = { vm::OP_START, vm::OP_CONST, 4, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode6[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode7[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; + static BlockFunc functions[] = { &PenBlocks::setPenColorToColor }; + static Value constValues[] = { "#AABbCC", "#03F", "#FFGFFF", "#AABBCCDD", "FFFFFF", 1228097602, 255 }; + + SpriteModel model; + Sprite sprite; + sprite.setInterface(&model); + + VirtualMachine vm(&sprite, &m_engineMock, nullptr); + vm.setBytecode(bytecode1); + vm.setFunctions(functions); + vm.setConstValues(constValues); + + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(210, 42, 204)); + + vm.reset(); + vm.setBytecode(bytecode2); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(228, 255, 255)); + + vm.reset(); + vm.setBytecode(bytecode3); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(359, 0, 0)); + + vm.reset(); + vm.setBytecode(bytecode4); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(359, 0, 0)); + + vm.reset(); + vm.setBytecode(bytecode5); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(359, 0, 0)); + + vm.reset(); + vm.setBytecode(bytecode6); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(162, 74, 72, 73)); + + vm.reset(); + vm.setBytecode(bytecode7); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(239, 255, 255)); +} + +TEST_F(PenBlocksTest, ChangePenColorParamBy) +{ + Compiler compiler(&m_engineMock); + + // change pen (color) by (34.6) + auto block1 = std::make_shared("a", "pen_changePenColorParamBy"); + addDropdownInput(block1, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "color"); + addValueInput(block1, "VALUE", PenBlocks::VALUE, 34.6); + + // change pen (saturation) by (46.8) + auto block2 = std::make_shared("b", "pen_changePenColorParamBy"); + addDropdownInput(block2, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "saturation"); + addValueInput(block2, "VALUE", PenBlocks::VALUE, 46.8); + + // change pen (brightness) by (0.45) + auto block3 = std::make_shared("c", "pen_changePenColorParamBy"); + addDropdownInput(block3, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "brightness"); + addValueInput(block3, "VALUE", PenBlocks::VALUE, 0.45); + + // change pen (transparency) by (89.06) + auto block4 = std::make_shared("d", "pen_changePenColorParamBy"); + addDropdownInput(block4, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "transparency"); + addValueInput(block4, "VALUE", PenBlocks::VALUE, 89.06); + + // change pen (invalid param) by (52.7) + auto block5 = std::make_shared("e", "pen_changePenColorParamBy"); + addDropdownInput(block5, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "invalid param"); + addValueInput(block5, "VALUE", PenBlocks::VALUE, 52.7); + + // change pen (null block) by (35.2) + auto block6 = std::make_shared("f", "pen_changePenColorParamBy"); + addDropdownInput(block6, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "", createNullBlock("g")); + addValueInput(block6, "VALUE", PenBlocks::VALUE, 35.2); + + compiler.init(); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenColorBy)).WillOnce(Return(0)); + compiler.setBlock(block1); + PenBlocks::compileChangePenColorParamBy(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenSaturationBy)).WillOnce(Return(1)); + compiler.setBlock(block2); + PenBlocks::compileChangePenColorParamBy(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenBrightnessBy)).WillOnce(Return(2)); + compiler.setBlock(block3); + PenBlocks::compileChangePenColorParamBy(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenTransparencyBy)).WillOnce(Return(3)); + compiler.setBlock(block4); + PenBlocks::compileChangePenColorParamBy(&compiler); + + compiler.setBlock(block5); + PenBlocks::compileChangePenColorParamBy(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenColorParamBy)).WillOnce(Return(4)); + compiler.setBlock(block6); + PenBlocks::compileChangePenColorParamBy(&compiler); + + compiler.end(); + + ASSERT_EQ( + compiler.bytecode(), + std::vector( + { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_CONST, 2, vm::OP_EXEC, 2, vm::OP_CONST, 3, vm::OP_EXEC, 3, + vm::OP_NULL, vm::OP_CONST, 4, vm::OP_EXEC, 4, vm::OP_HALT })); + ASSERT_EQ(compiler.constValues(), std::vector({ 34.6, 46.8, 0.45, 89.06, 35.2 })); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(PenBlocksTest, ChangePenColorParamByImpl) +{ + static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode4[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode5[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode6[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode7[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode8[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode9[] = { vm::OP_START, vm::OP_CONST, 4, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode10[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 1, vm::OP_HALT }; + static unsigned int bytecode11[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 1, vm::OP_HALT }; + static unsigned int bytecode12[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 2, vm::OP_HALT }; + static unsigned int bytecode13[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 2, vm::OP_HALT }; + static unsigned int bytecode14[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 3, vm::OP_HALT }; + static unsigned int bytecode15[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 3, vm::OP_HALT }; + static unsigned int bytecode16[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 4, vm::OP_HALT }; + static unsigned int bytecode17[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 4, vm::OP_HALT }; + static BlockFunc + functions[] = { &PenBlocks::changePenColorParamBy, &PenBlocks::changePenColorBy, &PenBlocks::changePenSaturationBy, &PenBlocks::changePenBrightnessBy, &PenBlocks::changePenTransparencyBy }; + static Value constValues[] = { "color", "saturation", "brightness", "transparency", "invalid", 53.2, -120.8 }; + + SpriteModel model; + model.penState().transparency = 100 * (1 - 150 / 255.0); + Sprite sprite; + sprite.setInterface(&model); + + VirtualMachine vm(&sprite, &m_engineMock, nullptr); + vm.setBytecode(bytecode1); + vm.setFunctions(functions); + vm.setConstValues(constValues); + + // color + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(71, 255, 255, 150)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(263, 255, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode2); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 255, 255, 150)); + + // saturation + model.penState().saturation = 32.4; + vm.reset(); + vm.setBytecode(bytecode3); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 218, 255, 150)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 255, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode4); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 255, 150)); + + // brightness + model.penState().brightness = 12.5; + vm.reset(); + vm.setBytecode(bytecode5); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 167, 150)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode6); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 0, 150)); + + // transparency + model.penState().transparency = 6.28; + vm.reset(); + vm.setBytecode(bytecode7); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 0, 103)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 0, 0)); + + vm.reset(); + vm.setBytecode(bytecode8); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 0, 255)); + + // invalid parameter + vm.reset(); + vm.setBytecode(bytecode9); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 0, 255)); + + // color (optimized) + model.penState() = PenState(); + model.penState().transparency = 100 * (1 - 150 / 255.0); + vm.reset(); + vm.setBytecode(bytecode10); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(71, 255, 255, 150)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(263, 255, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode11); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 255, 255, 150)); + + // saturation (optimized) + model.penState().saturation = 32.4; + vm.reset(); + vm.setBytecode(bytecode12); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 218, 255, 150)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 255, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode13); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 255, 150)); + + // brightness (optimized) + model.penState().brightness = 12.5; + vm.reset(); + vm.setBytecode(bytecode14); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 167, 150)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode15); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 0, 150)); + + // transparency (optimized) + model.penState().transparency = 6.28; + vm.reset(); + vm.setBytecode(bytecode16); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 0, 103)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 0, 0)); + + vm.reset(); + vm.setBytecode(bytecode17); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(188, 0, 0, 255)); +} + +TEST_F(PenBlocksTest, SetPenColorParamTo) +{ + Compiler compiler(&m_engineMock); + + // set pen (color) to (34.6) + auto block1 = std::make_shared("a", "pen_setPenColorParamTo"); + addDropdownInput(block1, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "color"); + addValueInput(block1, "VALUE", PenBlocks::VALUE, 34.6); + + // set pen (saturation) to (46.8) + auto block2 = std::make_shared("b", "pen_setPenColorParamTo"); + addDropdownInput(block2, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "saturation"); + addValueInput(block2, "VALUE", PenBlocks::VALUE, 46.8); + + // set pen (brightness) to (0.45) + auto block3 = std::make_shared("c", "pen_setPenColorParamTo"); + addDropdownInput(block3, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "brightness"); + addValueInput(block3, "VALUE", PenBlocks::VALUE, 0.45); + + // set pen (transparency) to (89.06) + auto block4 = std::make_shared("d", "pen_setPenColorParamTo"); + addDropdownInput(block4, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "transparency"); + addValueInput(block4, "VALUE", PenBlocks::VALUE, 89.06); + + // set pen (invalid param) to (52.7) + auto block5 = std::make_shared("e", "pen_setPenColorParamTo"); + addDropdownInput(block5, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "invalid param"); + addValueInput(block5, "VALUE", PenBlocks::VALUE, 52.7); + + // set pen (null block) to (35.2) + auto block6 = std::make_shared("f", "pen_setPenColorParamTo"); + addDropdownInput(block6, "COLOR_PARAM", PenBlocks::COLOR_PARAM, "", createNullBlock("g")); + addValueInput(block6, "VALUE", PenBlocks::VALUE, 35.2); + + compiler.init(); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenColorTo)).WillOnce(Return(0)); + compiler.setBlock(block1); + PenBlocks::compileSetPenColorParamTo(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenSaturationTo)).WillOnce(Return(1)); + compiler.setBlock(block2); + PenBlocks::compileSetPenColorParamTo(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenBrightnessTo)).WillOnce(Return(2)); + compiler.setBlock(block3); + PenBlocks::compileSetPenColorParamTo(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenTransparencyTo)).WillOnce(Return(3)); + compiler.setBlock(block4); + PenBlocks::compileSetPenColorParamTo(&compiler); + + compiler.setBlock(block5); + PenBlocks::compileSetPenColorParamTo(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenColorParamTo)).WillOnce(Return(4)); + compiler.setBlock(block6); + PenBlocks::compileSetPenColorParamTo(&compiler); + + compiler.end(); + + ASSERT_EQ( + compiler.bytecode(), + std::vector( + { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_CONST, 1, vm::OP_EXEC, 1, vm::OP_CONST, 2, vm::OP_EXEC, 2, vm::OP_CONST, 3, vm::OP_EXEC, 3, + vm::OP_NULL, vm::OP_CONST, 4, vm::OP_EXEC, 4, vm::OP_HALT })); + ASSERT_EQ(compiler.constValues(), std::vector({ 34.6, 46.8, 0.45, 89.06, 35.2 })); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(PenBlocksTest, SetPenColorParamToImpl) +{ + static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_CONST, 7, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode4[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode5[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode6[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_CONST, 7, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode7[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode8[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode9[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_CONST, 7, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode10[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode11[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_CONST, 6, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode12[] = { vm::OP_START, vm::OP_CONST, 3, vm::OP_CONST, 7, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode13[] = { vm::OP_START, vm::OP_CONST, 4, vm::OP_CONST, 5, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode14[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 1, vm::OP_HALT }; + static unsigned int bytecode15[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 1, vm::OP_HALT }; + static unsigned int bytecode16[] = { vm::OP_START, vm::OP_CONST, 7, vm::OP_EXEC, 1, vm::OP_HALT }; + static unsigned int bytecode17[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 2, vm::OP_HALT }; + static unsigned int bytecode18[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 2, vm::OP_HALT }; + static unsigned int bytecode19[] = { vm::OP_START, vm::OP_CONST, 7, vm::OP_EXEC, 2, vm::OP_HALT }; + static unsigned int bytecode20[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 3, vm::OP_HALT }; + static unsigned int bytecode21[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 3, vm::OP_HALT }; + static unsigned int bytecode22[] = { vm::OP_START, vm::OP_CONST, 7, vm::OP_EXEC, 3, vm::OP_HALT }; + static unsigned int bytecode23[] = { vm::OP_START, vm::OP_CONST, 5, vm::OP_EXEC, 4, vm::OP_HALT }; + static unsigned int bytecode24[] = { vm::OP_START, vm::OP_CONST, 6, vm::OP_EXEC, 4, vm::OP_HALT }; + static unsigned int bytecode25[] = { vm::OP_START, vm::OP_CONST, 7, vm::OP_EXEC, 4, vm::OP_HALT }; + static BlockFunc functions[] = { &PenBlocks::setPenColorParamTo, &PenBlocks::setPenColorTo, &PenBlocks::setPenSaturationTo, &PenBlocks::setPenBrightnessTo, &PenBlocks::setPenTransparencyTo }; + static Value constValues[] = { "color", "saturation", "brightness", "transparency", "invalid", 53.2, -234.9, 287.1 }; + + SpriteModel model; + model.penState().color = 78.6; + model.penState().transparency = 100 * (1 - 150 / 255.0); + Sprite sprite; + sprite.setInterface(&model); + + VirtualMachine vm(&sprite, &m_engineMock, nullptr); + vm.setBytecode(bytecode1); + vm.setFunctions(functions); + vm.setConstValues(constValues); + + // color + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(191, 255, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode2); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(234, 255, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode3); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 150)); + + // saturation + model.penState().saturation = 32.4; + vm.reset(); + vm.setBytecode(bytecode4); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 135, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode5); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 0, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode6); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 150)); + + // brightness + model.penState().brightness = 12.5; + vm.reset(); + vm.setBytecode(bytecode7); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 135, 150)); + + vm.reset(); + vm.setBytecode(bytecode8); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 0, 150)); + + vm.reset(); + vm.setBytecode(bytecode9); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 150)); + + // transparency + model.penState().transparency = 12.5; + vm.reset(); + vm.setBytecode(bytecode10); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 119)); + + vm.reset(); + vm.setBytecode(bytecode11); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 255)); + + vm.reset(); + vm.setBytecode(bytecode12); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 0)); + + // invalid parameter + vm.reset(); + vm.setBytecode(bytecode13); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 0)); + + // color (optimized) + model.penState() = PenState(); + model.penState().color = 78.6; + model.penState().transparency = 100 * (1 - 150 / 255.0); + vm.reset(); + vm.setBytecode(bytecode14); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(191, 255, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode15); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(234, 255, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode16); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 150)); + + // saturation (optimized) + model.penState().saturation = 32.4; + vm.reset(); + vm.setBytecode(bytecode17); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 135, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode18); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 0, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode19); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 150)); + + // brightness (optimized) + model.penState().brightness = 12.5; + vm.reset(); + vm.setBytecode(bytecode20); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 135, 150)); + + vm.reset(); + vm.setBytecode(bytecode21); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 0, 150)); + + vm.reset(); + vm.setBytecode(bytecode22); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 150)); + + // transparency (optimized) + model.penState().transparency = 12.5; + vm.reset(); + vm.setBytecode(bytecode23); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 119)); + + vm.reset(); + vm.setBytecode(bytecode24); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 255)); + + vm.reset(); + vm.setBytecode(bytecode25); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(313, 255, 255, 0)); +} + +TEST_F(PenBlocksTest, ChangePenSizeBy) +{ + Compiler compiler(&m_engineMock); + + // change pen size by (4.5) + auto block1 = std::make_shared("a", "pen_changePenSizeBy"); + addValueInput(block1, "SIZE", PenBlocks::SIZE, 4.5); + + // change pen size by (null block) + auto block2 = std::make_shared("b", "pen_changePenSizeBy"); + addObscuredInput(block2, "SIZE", PenBlocks::SIZE, createNullBlock("c")); + + compiler.init(); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenSizeBy)).WillOnce(Return(2)); + compiler.setBlock(block1); + PenBlocks::compileChangePenSizeBy(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenSizeBy)).WillOnce(Return(2)); + compiler.setBlock(block2); + PenBlocks::compileChangePenSizeBy(&compiler); + + compiler.end(); + + ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); + ASSERT_EQ(compiler.constValues().size(), 1); + ASSERT_EQ(compiler.constValues()[0].toDouble(), 4.5); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(PenBlocksTest, ChangePenSizeByImpl) +{ + static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; + static BlockFunc functions[] = { &PenBlocks::changePenSizeBy }; + static Value constValues[] = { 511.5, -650.08 }; + + SpriteModel model; + Sprite sprite; + sprite.setInterface(&model); + + VirtualMachine vm(&sprite, &m_engineMock, nullptr); + vm.setBytecode(bytecode1); + vm.setFunctions(functions); + vm.setConstValues(constValues); + + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().diameter, 512.5); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().diameter, 1024); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().diameter, 1200); + + vm.reset(); + vm.setBytecode(bytecode2); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().diameter, 549.92); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().diameter, 1); +} + +TEST_F(PenBlocksTest, SetPenSizeTo) +{ + Compiler compiler(&m_engineMock); + + // set pen size to (51.46) + auto block1 = std::make_shared("a", "pen_setPenSizeTo"); + addValueInput(block1, "SIZE", PenBlocks::SIZE, 51.46); + + // set pen size to (null block) + auto block2 = std::make_shared("b", "pen_setPenSizeTo"); + addObscuredInput(block2, "SIZE", PenBlocks::SIZE, createNullBlock("c")); + + compiler.init(); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenSizeTo)).WillOnce(Return(2)); + compiler.setBlock(block1); + PenBlocks::compileSetPenSizeTo(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenSizeTo)).WillOnce(Return(2)); + compiler.setBlock(block2); + PenBlocks::compileSetPenSizeTo(&compiler); + + compiler.end(); + + ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); + ASSERT_EQ(compiler.constValues().size(), 1); + ASSERT_EQ(compiler.constValues()[0].toDouble(), 51.46); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(PenBlocksTest, SetPenSizeToImpl) +{ + static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 0, vm::OP_HALT }; + static BlockFunc functions[] = { &PenBlocks::setPenSizeTo }; + static Value constValues[] = { 511.5, -650.08, 1500 }; + + SpriteModel model; + Sprite sprite; + sprite.setInterface(&model); + + VirtualMachine vm(&sprite, &m_engineMock, nullptr); + vm.setBytecode(bytecode1); + vm.setFunctions(functions); + vm.setConstValues(constValues); + + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().diameter, 511.5); + + vm.reset(); + vm.setBytecode(bytecode2); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().diameter, 1); + + vm.reset(); + vm.setBytecode(bytecode3); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().diameter, 1200); +} + +TEST_F(PenBlocksTest, ChangePenShadeBy) +{ + Compiler compiler(&m_engineMock); + + // change pen shade by (4.5) + auto block1 = std::make_shared("a", "pen_changePenShadeBy"); + addValueInput(block1, "SHADE", PenBlocks::SHADE, 4.5); + + // change pen shade by (null block) + auto block2 = std::make_shared("b", "pen_changePenShadeBy"); + addObscuredInput(block2, "SHADE", PenBlocks::SHADE, createNullBlock("c")); + + compiler.init(); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenShadeBy)).WillOnce(Return(2)); + compiler.setBlock(block1); + PenBlocks::compileChangePenShadeBy(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenShadeBy)).WillOnce(Return(2)); + compiler.setBlock(block2); + PenBlocks::compileChangePenShadeBy(&compiler); + + compiler.end(); + + ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); + ASSERT_EQ(compiler.constValues().size(), 1); + ASSERT_EQ(compiler.constValues()[0].toDouble(), 4.5); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(PenBlocksTest, ChangePenShadeByImpl) +{ + static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; + static BlockFunc functions[] = { &PenBlocks::changePenShadeBy }; + static Value constValues[] = { 134.09, -124.45 }; + + SpriteModel model; + model.penState().transparency = 100 * (1 - 150 / 255.0); + Sprite sprite; + sprite.setInterface(&model); + + VirtualMachine vm(&sprite, &m_engineMock, nullptr); + vm.setBytecode(bytecode1); + vm.setFunctions(functions); + vm.setConstValues(constValues); + + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(240, 255, 110, 150)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(240, 119, 255, 150)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(240, 247, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode2); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(240, 162, 255, 150)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(240, 255, 55, 150)); +} + +TEST_F(PenBlocksTest, SetPenShadeToNumber) +{ + Compiler compiler(&m_engineMock); + + // set pen shade to (4.5) + auto block1 = std::make_shared("a", "pen_setPenShadeToNumber"); + addValueInput(block1, "SHADE", PenBlocks::SHADE, 4.5); + + // set pen shade to (null block) + auto block2 = std::make_shared("b", "pen_setPenShadeToNumber"); + addObscuredInput(block2, "SHADE", PenBlocks::SHADE, createNullBlock("c")); + + compiler.init(); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenShadeToNumber)).WillOnce(Return(2)); + compiler.setBlock(block1); + PenBlocks::compileSetPenShadeToNumber(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenShadeToNumber)).WillOnce(Return(2)); + compiler.setBlock(block2); + PenBlocks::compileSetPenShadeToNumber(&compiler); + + compiler.end(); + + ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); + ASSERT_EQ(compiler.constValues().size(), 1); + ASSERT_EQ(compiler.constValues()[0].toDouble(), 4.5); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(PenBlocksTest, SetPenShadeToNumberImpl) +{ + static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 0, vm::OP_HALT }; + static BlockFunc functions[] = { &PenBlocks::setPenShadeToNumber }; + static Value constValues[] = { 125.7, -114.09, 489.4 }; + + SpriteModel model; + model.penState().transparency = 100 * (1 - 150 / 255.0); + Sprite sprite; + sprite.setInterface(&model); + + VirtualMachine vm(&sprite, &m_engineMock, nullptr); + vm.setBytecode(bytecode1); + vm.setFunctions(functions); + vm.setConstValues(constValues); + + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(240, 148, 253, 150)); + + vm.reset(); + vm.setBytecode(bytecode2); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(240, 102, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode3); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(240, 89, 255, 150)); +} + +TEST_F(PenBlocksTest, ChangePenHueBy) +{ + Compiler compiler(&m_engineMock); + + // change pen hue by (4.5) + auto block1 = std::make_shared("a", "pen_changePenHueBy"); + addValueInput(block1, "HUE", PenBlocks::HUE, 4.5); + + // change pen hue by (null block) + auto block2 = std::make_shared("b", "pen_changePenHueBy"); + addObscuredInput(block2, "HUE", PenBlocks::HUE, createNullBlock("c")); + + compiler.init(); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenHueBy)).WillOnce(Return(2)); + compiler.setBlock(block1); + PenBlocks::compileChangePenHueBy(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::changePenHueBy)).WillOnce(Return(2)); + compiler.setBlock(block2); + PenBlocks::compileChangePenHueBy(&compiler); + + compiler.end(); + + ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); + ASSERT_EQ(compiler.constValues().size(), 1); + ASSERT_EQ(compiler.constValues()[0].toDouble(), 4.5); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(PenBlocksTest, ChangePenHueByImpl) +{ + static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; + static BlockFunc functions[] = { &PenBlocks::changePenHueBy }; + static Value constValues[] = { 125.7, -114.09 }; + + SpriteModel model; + model.penState().transparency = 100 * (1 - 150 / 255.0); + Sprite sprite; + sprite.setInterface(&model); + + VirtualMachine vm(&sprite, &m_engineMock, nullptr); + vm.setBytecode(bytecode1); + vm.setFunctions(functions); + vm.setConstValues(constValues); + + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(106, 255, 255, 150)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(332, 255, 255, 150)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(199, 255, 255, 150)); + + vm.reset(); + vm.setBytecode(bytecode2); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(353, 255, 255, 150)); + + vm.reset(); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(148, 255, 255, 150)); +} + +TEST_F(PenBlocksTest, SetPenHueToNumber) +{ + Compiler compiler(&m_engineMock); + + // set pen hue to (54.09) + auto block1 = std::make_shared("a", "pen_setPenHueToNumber"); + addValueInput(block1, "HUE", PenBlocks::HUE, 54.09); + + // set pen hue to (null block) + auto block2 = std::make_shared("b", "pen_setPenHueToNumber"); + addObscuredInput(block2, "HUE", PenBlocks::HUE, createNullBlock("c")); + + compiler.init(); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenHueToNumber)).WillOnce(Return(2)); + compiler.setBlock(block1); + PenBlocks::compileSetPenHueToNumber(&compiler); + + EXPECT_CALL(m_engineMock, functionIndex(&PenBlocks::setPenHueToNumber)).WillOnce(Return(2)); + compiler.setBlock(block2); + PenBlocks::compileSetPenHueToNumber(&compiler); + + compiler.end(); + + ASSERT_EQ(compiler.bytecode(), std::vector({ vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 2, vm::OP_NULL, vm::OP_EXEC, 2, vm::OP_HALT })); + ASSERT_EQ(compiler.constValues().size(), 1); + ASSERT_EQ(compiler.constValues()[0].toDouble(), 54.09); + ASSERT_TRUE(compiler.variables().empty()); + ASSERT_TRUE(compiler.lists().empty()); +} + +TEST_F(PenBlocksTest, SetPenHueToNumberImpl) +{ + static unsigned int bytecode1[] = { vm::OP_START, vm::OP_CONST, 0, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode2[] = { vm::OP_START, vm::OP_CONST, 1, vm::OP_EXEC, 0, vm::OP_HALT }; + static unsigned int bytecode3[] = { vm::OP_START, vm::OP_CONST, 2, vm::OP_EXEC, 0, vm::OP_HALT }; + static BlockFunc functions[] = { &PenBlocks::setPenHueToNumber }; + static Value constValues[] = { 125.7, -114.09, 489.4 }; + + SpriteModel model; + model.penState().transparency = 100 * (1 - 150 / 255.0); + Sprite sprite; + sprite.setInterface(&model); + + VirtualMachine vm(&sprite, &m_engineMock, nullptr); + vm.setBytecode(bytecode1); + vm.setFunctions(functions); + vm.setConstValues(constValues); + + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(226, 255, 255, 255)); + + vm.reset(); + vm.setBytecode(bytecode2); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(154, 255, 255, 255)); + + vm.reset(); + vm.setBytecode(bytecode3); + vm.run(); + ASSERT_EQ(vm.registerCount(), 0); + ASSERT_EQ(model.penAttributes().color, QColor::fromHsv(160, 255, 255, 255)); +} diff --git a/test/penstate/CMakeLists.txt b/test/penstate/CMakeLists.txt new file mode 100644 index 0000000..2376ba7 --- /dev/null +++ b/test/penstate/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable( + penstate_test + penstate_test.cpp +) + +target_link_libraries( + penstate_test + GTest::gtest_main + scratchcpp-render + qnanopainter +) + +add_test(penstate_test) +gtest_discover_tests(penstate_test) diff --git a/test/penstate/penstate_test.cpp b/test/penstate/penstate_test.cpp new file mode 100644 index 0000000..9a7be7d --- /dev/null +++ b/test/penstate/penstate_test.cpp @@ -0,0 +1,32 @@ +#include + +#include "../common.h" + +using namespace scratchcpprender; + +TEST(PenStateTest, DefaultPenState) +{ + PenState state; + ASSERT_FALSE(state.penDown); + ASSERT_EQ(state.color, 66.66); + ASSERT_EQ(state.saturation, 100); + ASSERT_EQ(state.brightness, 100); + ASSERT_EQ(state.transparency, 0); + ASSERT_EQ(state.shade, 50); + + PenAttributes defaultAttributes; + ASSERT_EQ(state.penAttributes.color, defaultAttributes.color); + ASSERT_EQ(state.penAttributes.diameter, defaultAttributes.diameter); +} + +TEST(PenStateTest, UpdateColor) +{ + PenState state; + state.color = 78.64; + state.saturation = 45.07; + state.brightness = 12.5; + state.transparency = 36.09; + state.shade = 85; + state.updateColor(); + ASSERT_EQ(state.penAttributes.color, QColor::fromHsv(283, 114, 31, 162)); +} diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp index 7d932cd..55a2020 100644 --- a/test/target_models/spritemodel_test.cpp +++ b/test/target_models/spritemodel_test.cpp @@ -268,6 +268,7 @@ TEST(SpriteModelTest, PenDown) sprite.setEngine(&engine); model.init(&sprite); ASSERT_FALSE(model.penDown()); + ASSERT_FALSE(model.penState().penDown); PenLayerMock penLayer; model.setPenLayer(&penLayer); @@ -278,14 +279,17 @@ TEST(SpriteModelTest, PenDown) EXPECT_CALL(engine, requestRedraw()); model.setPenDown(true); ASSERT_TRUE(model.penDown()); + ASSERT_TRUE(model.penState().penDown); EXPECT_CALL(penLayer, drawPoint(_, 24.6, -48.8)); EXPECT_CALL(engine, requestRedraw()); model.setPenDown(true); ASSERT_TRUE(model.penDown()); + ASSERT_TRUE(model.penState().penDown); EXPECT_CALL(penLayer, drawPoint).Times(0); EXPECT_CALL(engine, requestRedraw).Times(0); model.setPenDown(false); ASSERT_FALSE(model.penDown()); + ASSERT_FALSE(model.penState().penDown); }