diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5cd33b5..bb1993a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -31,6 +31,14 @@ qt_add_qml_module(scratchcpp-render listmonitorlistmodel.cpp listmonitorlistmodel.h irenderedtarget.h + texture.cpp + texture.h + skin.cpp + skin.h + bitmapskin.cpp + bitmapskin.h + svgskin.cpp + svgskin.h renderedtarget.cpp renderedtarget.h targetpainter.cpp diff --git a/src/bitmapskin.cpp b/src/bitmapskin.cpp new file mode 100644 index 0000000..a3adb44 --- /dev/null +++ b/src/bitmapskin.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include "bitmapskin.h" + +using namespace scratchcpprender; + +BitmapSkin::BitmapSkin(libscratchcpp::Costume *costume) : + Skin(costume) +{ + if (!costume) + return; + + // Read image data + QBuffer buffer; + buffer.open(QBuffer::WriteOnly); + buffer.write(static_cast(costume->data()), costume->dataSize()); + buffer.close(); + const char *format; + + { + QImageReader reader(&buffer); + format = reader.format(); + } + + buffer.close(); + m_image.load(&buffer, format); + + // Paint the image into a texture + m_texture = createAndPaintTexture(m_image.width(), m_image.height(), false); + m_textureSize.setWidth(m_image.width()); + m_textureSize.setHeight(m_image.height()); + Q_ASSERT(m_texture.isValid()); +} + +BitmapSkin::~BitmapSkin() +{ + m_texture.release(); +} + +Texture BitmapSkin::getTexture(double scale) const +{ + return m_texture; +} + +double BitmapSkin::getTextureScale(const Texture &texture) const +{ + return 1; +} + +void BitmapSkin::paint(QPainter *painter) +{ + painter->drawImage(m_image.rect(), m_image, m_image.rect()); +} diff --git a/src/bitmapskin.h b/src/bitmapskin.h new file mode 100644 index 0000000..0979ae1 --- /dev/null +++ b/src/bitmapskin.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include "skin.h" +#include "texture.h" + +namespace scratchcpprender +{ + +class BitmapSkin : public Skin +{ + public: + BitmapSkin(libscratchcpp::Costume *costume); + ~BitmapSkin(); + + Texture getTexture(double scale) const override; + double getTextureScale(const Texture &texture) const override; + + protected: + void paint(QPainter *painter) override; + + private: + Texture m_texture; + QSize m_textureSize; + QImage m_image; +}; + +} // namespace scratchcpprender diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h index d3295ae..2a2d1b9 100644 --- a/src/irenderedtarget.h +++ b/src/irenderedtarget.h @@ -2,6 +2,7 @@ #pragma once +#include #include #include @@ -15,6 +16,7 @@ namespace scratchcpprender class StageModel; class SpriteModel; class SceneMouseArea; +class Texture; class IRenderedTarget : public QNanoQuickItem { @@ -33,8 +35,10 @@ class IRenderedTarget : public QNanoQuickItem virtual void updateDirection(double direction) = 0; virtual void updateRotationStyle(libscratchcpp::Sprite::RotationStyle style) = 0; virtual void updateLayerOrder(int layerOrder) = 0; + virtual void updateCostume(libscratchcpp::Costume *costume) = 0; - virtual void loadCostume(libscratchcpp::Costume *costume) = 0; + virtual bool costumesLoaded() const = 0; + virtual void loadCostumes() = 0; virtual void beforeRedraw() = 0; @@ -63,18 +67,13 @@ class IRenderedTarget : public QNanoQuickItem virtual qreal height() const = 0; virtual void setHeight(qreal width) = 0; - virtual QPointF mapFromScene(const QPointF &point) const = 0; - - virtual QBuffer *bitmapBuffer() = 0; - virtual const QString &bitmapUniqueKey() const = 0; + virtual libscratchcpp::Rect getBounds() const = 0; - virtual void lockCostume() = 0; - virtual void unlockCostume() = 0; + virtual QPointF mapFromScene(const QPointF &point) const = 0; virtual bool mirrorHorizontally() const = 0; - virtual bool isSvg() const = 0; - virtual void paintSvg(QNanoPainter *painter) = 0; + virtual Texture texture() const = 0; virtual void updateHullPoints(QOpenGLFramebufferObject *fbo) = 0; virtual const std::vector &hullPoints() const = 0; diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 0d3dfd8..96892e8 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -10,33 +11,25 @@ #include "stagemodel.h" #include "spritemodel.h" #include "scenemousearea.h" +#include "bitmapskin.h" +#include "svgskin.h" using namespace scratchcpprender; using namespace libscratchcpp; static const double SVG_SCALE_LIMIT = 0.1; // the maximum viewport dimensions are multiplied by this +static const double pi = std::acos(-1); // TODO: Use std::numbers::pi in C++20 RenderedTarget::RenderedTarget(QNanoQuickItem *parent) : IRenderedTarget(parent) { - // Get maximum viewport dimensions - QOpenGLContext context; - context.create(); - Q_ASSERT(context.isValid()); - - if (context.isValid()) { - QOffscreenSurface surface; - surface.create(); - Q_ASSERT(surface.isValid()); - - if (surface.isValid()) { - context.makeCurrent(&surface); - GLint dims[2]; - glGetIntegerv(GL_MAX_VIEWPORT_DIMS, dims); - m_maximumWidth = dims[0] * SVG_SCALE_LIMIT; - m_maximumHeight = dims[1] * SVG_SCALE_LIMIT; - context.doneCurrent(); - } +} + +RenderedTarget::~RenderedTarget() +{ + if (!m_skinsInherited) { + for (const auto &[costume, skin] : m_skins) + delete skin; } } @@ -104,49 +97,82 @@ void RenderedTarget::updateLayerOrder(int layerOrder) setZ(layerOrder); } -void RenderedTarget::loadCostume(Costume *costume) +void RenderedTarget::updateCostume(Costume *costume) { if (!costume || costume == m_costume) return; - m_costumeMutex.lock(); m_costume = costume; - if (m_costume->dataFormat() == "svg") { - m_svgRenderer.load(QByteArray::fromRawData(static_cast(m_costume->data()), m_costume->dataSize())); - QRectF rect = m_svgRenderer.viewBoxF(); - m_costumeWidth = rect.width(); - m_costumeHeight = rect.height(); - } else { - m_bitmapBuffer.open(QBuffer::WriteOnly); - m_bitmapBuffer.write(static_cast(m_costume->data()), m_costume->dataSize()); - m_bitmapBuffer.close(); - m_bitmapUniqueKey = QString::fromStdString(m_costume->id()); - const char *format; - - { - QImageReader reader(&m_bitmapBuffer); - format = reader.format(); - } + if (m_costumesLoaded) { + auto it = m_skins.find(m_costume); - m_bitmapBuffer.close(); - m_costumeBitmap.load(&m_bitmapBuffer, format); - QSize size = m_costumeBitmap.size(); - m_costumeWidth = std::max(0, size.width()); - m_costumeHeight = std::max(0, size.height()); - m_bitmapBuffer.close(); + if (it == m_skins.end()) + m_skin = nullptr; + else + m_skin = it->second; } - m_costumeMutex.unlock(); - calculateSize(); calculatePos(); } +bool RenderedTarget::costumesLoaded() const +{ + return m_costumesLoaded; +} + +void RenderedTarget::loadCostumes() +{ + // Delete previous skins + if (!m_skinsInherited) { + for (const auto &[costume, skin] : m_skins) + delete skin; + } + + m_skinsInherited = false; + m_skins.clear(); + + // Generate a skin for each costume + Target *target = scratchTarget(); + + if (!target) + return; + + const auto &costumes = target->costumes(); + + for (auto costume : costumes) { + Skin *skin = nullptr; + if (costume->dataFormat() == "svg") + skin = new SVGSkin(costume.get()); + else + skin = new BitmapSkin(costume.get()); + + if (skin) + m_skins[costume.get()] = skin; + + if (m_costume && costume.get() == m_costume) + m_skin = skin; + } + + m_costumesLoaded = true; + + if (m_costume) { + calculateSize(); + calculatePos(); + } +} + void RenderedTarget::beforeRedraw() { + // These properties must be set here to avoid unnecessary calls to update() setWidth(m_width); setHeight(m_height); + + if (!m_oldTexture.isValid() || (m_texture.isValid() && m_texture != m_oldTexture)) { + m_oldTexture = m_texture; + update(); + } } void RenderedTarget::deinitClone() @@ -189,7 +215,7 @@ void RenderedTarget::setStageModel(StageModel *newStageModel) Stage *stage = m_stageModel->stage(); if (stage) - loadCostume(stage->currentCostume().get()); + updateCostume(stage->currentCostume().get()); } emit stageModelChanged(); @@ -208,6 +234,20 @@ void RenderedTarget::setSpriteModel(SpriteModel *newSpriteModel) m_spriteModel = newSpriteModel; if (m_spriteModel) { + SpriteModel *cloneRoot = m_spriteModel->cloneRoot(); + + if (cloneRoot) { + // Inherit skins from the clone root + RenderedTarget *target = dynamic_cast(cloneRoot->renderedTarget()); + Q_ASSERT(target); + + if (target->costumesLoaded()) { + m_skins = target->m_skins; // TODO: Avoid copying - maybe using a pointer? + m_costumesLoaded = true; + m_skinsInherited = true; // avoid double free + } + } + Sprite *sprite = m_spriteModel->sprite(); if (sprite) { @@ -216,7 +256,7 @@ void RenderedTarget::setSpriteModel(SpriteModel *newSpriteModel) m_size = sprite->size() / 100; m_direction = sprite->direction(); m_rotationStyle = sprite->rotationStyle(); - loadCostume(sprite->currentCostume().get()); + updateCostume(sprite->currentCostume().get()); updateVisibility(sprite->visible()); updateLayerOrder(sprite->layerOrder()); calculateSize(); @@ -289,6 +329,43 @@ void RenderedTarget::setHeight(qreal height) QNanoQuickItem::setHeight(height); } +Rect RenderedTarget::getBounds() const +{ + // https://github.com/scratchfoundation/scratch-render/blob/c3ede9c3d54769730c7b023021511e2aba167b1f/src/Rectangle.js#L33-L55 + if (!m_costume || !m_skin || !m_texture.isValid()) + return Rect(m_x, m_y, m_x, m_y); + + const double width = m_texture.width() * m_size / scale() / m_costume->bitmapResolution(); + const double height = m_texture.height() * m_size / scale() / m_costume->bitmapResolution(); + const double originX = m_stageScale * m_costume->rotationCenterX() * m_size / scale() / m_costume->bitmapResolution() - width / 2; + const double originY = m_stageScale * -m_costume->rotationCenterY() * m_size / scale() / m_costume->bitmapResolution() + height / 2; + const double rot = -rotation() * pi / 180; + double left = std::numeric_limits::infinity(); + double top = -std::numeric_limits::infinity(); + double right = -std::numeric_limits::infinity(); + double bottom = std::numeric_limits::infinity(); + + for (const QPointF &point : m_hullPoints) { + QPointF transformed = transformPoint(point.x() - width / 2, height / 2 - point.y(), originX, originY, rot); + const double x = transformed.x() * scale() / m_stageScale * (m_mirrorHorizontally ? -1 : 1); + const double y = transformed.y() * scale() / m_stageScale; + + if (x < left) + left = x; + + if (x > right) + right = x; + + if (y > top) + top = y; + + if (y < bottom) + bottom = y; + } + + return Rect(left + m_x, top + m_y, right + m_x, bottom + m_y); +} + QPointF RenderedTarget::mapFromScene(const QPointF &point) const { return QNanoQuickItem::mapFromScene(point); @@ -343,35 +420,9 @@ void RenderedTarget::mouseMoveEvent(QMouseEvent *event) } } -void RenderedTarget::paintSvg(QNanoPainter *painter) +Texture RenderedTarget::texture() const { - Q_ASSERT(painter); - QOpenGLContext *context = QOpenGLContext::currentContext(); - Q_ASSERT(context); - - if (!context) - return; - - QOffscreenSurface surface; - surface.setFormat(context->format()); - surface.create(); - Q_ASSERT(surface.isValid()); - - QSurface *oldSurface = context->surface(); - context->makeCurrent(&surface); - - const QRectF drawRect(0, 0, std::min(width(), m_maximumWidth), std::min(height(), m_maximumHeight)); - const QSize drawRectSize = drawRect.size().toSize(); - - QOpenGLPaintDevice device(drawRectSize); - QPainter qPainter; - qPainter.begin(&device); - qPainter.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); - m_svgRenderer.render(&qPainter, drawRect); - qPainter.end(); - - context->doneCurrent(); - context->makeCurrent(oldSurface); + return m_texture; } void RenderedTarget::updateHullPoints(QOpenGLFramebufferObject *fbo) @@ -385,19 +436,10 @@ void RenderedTarget::updateHullPoints(QOpenGLFramebufferObject *fbo) m_hullPoints.clear(); m_hullPoints.reserve(width * height); - // Blit multisampled FBO to a custom FBO - QOpenGLFramebufferObject customFbo(fbo->size()); - glBindFramebuffer(GL_READ_FRAMEBUFFER_EXT, fbo->handle()); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER_EXT, customFbo.handle()); - glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST); - glBindFramebuffer(GL_FRAMEBUFFER_EXT, customFbo.handle()); - // Read pixels from framebuffer size_t size = width * height * 4; GLubyte *pixelData = new GLubyte[size]; glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixelData); - glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0); - fbo->bind(); // Flip vertically int rowSize = width * 4; @@ -458,27 +500,24 @@ bool RenderedTarget::contains(const QPointF &point) const void RenderedTarget::calculatePos() { - if (!m_costume || !m_engine) + if (!m_skin || !m_costume || !m_engine) return; - if (m_spriteModel) { - if (isVisible()) { - double stageWidth = m_engine->stageWidth(); - double stageHeight = m_engine->stageHeight(); - setX(m_stageScale * (stageWidth / 2 + m_x - m_costume->rotationCenterX() * m_clampedSize / m_costume->bitmapResolution() * (m_mirrorHorizontally ? -1 : 1))); - setY(m_stageScale * (stageHeight / 2 - m_y - m_costume->rotationCenterY() * m_clampedSize / m_costume->bitmapResolution())); - qreal originX = m_costume->rotationCenterX() * m_clampedSize * m_stageScale / m_costume->bitmapResolution(); - qreal originY = m_costume->rotationCenterY() * m_clampedSize * m_stageScale / m_costume->bitmapResolution(); - setTransformOriginPoint(QPointF(originX, originY)); - } - } else { + if (isVisible() || m_stageModel) { double stageWidth = m_engine->stageWidth(); double stageHeight = m_engine->stageHeight(); - setX(m_stageScale * (stageWidth / 2 - m_costume->rotationCenterX() / m_costume->bitmapResolution())); - setY(m_stageScale * (stageHeight / 2 - m_costume->rotationCenterY() / m_costume->bitmapResolution())); - qreal originX = m_costume->rotationCenterX() / m_costume->bitmapResolution(); - qreal originY = m_costume->rotationCenterY() / m_costume->bitmapResolution(); + setX(m_stageScale * (stageWidth / 2 + m_x - m_costume->rotationCenterX() * m_size / scale() / m_costume->bitmapResolution() * (m_mirrorHorizontally ? -1 : 1))); + setY(m_stageScale * (stageHeight / 2 - m_y - m_costume->rotationCenterY() * m_size / scale() / m_costume->bitmapResolution())); + qreal originX = m_costume->rotationCenterX() * m_stageScale * m_size / scale() / m_costume->bitmapResolution(); + qreal originY = m_costume->rotationCenterY() * m_stageScale * m_size / scale() / m_costume->bitmapResolution(); setTransformOriginPoint(QPointF(originX, originY)); + + // Qt ignores the transform origin point if it's (0, 0), + // so set the transform origin to top left in this case. + if (originX == 0 && originY == 0) + setTransformOrigin(QQuickItem::TopLeft); + else + setTransformOrigin(QQuickItem::Center); } } @@ -515,30 +554,13 @@ void RenderedTarget::calculateRotation() void RenderedTarget::calculateSize() { - if (m_costume) { - double bitmapRes = m_costume->bitmapResolution(); - - if (m_costumeWidth == 0 || m_costumeHeight == 0) - m_maxSize = 1; - else - m_maxSize = std::min(m_maximumWidth / (m_costumeWidth * m_stageScale), m_maximumHeight / (m_costumeHeight * m_stageScale)); - - if (m_spriteModel) { - m_clampedSize = std::min(m_size, m_maxSize); - m_width = m_costumeWidth * m_clampedSize * m_stageScale / bitmapRes; - m_height = m_height = m_costumeHeight * m_clampedSize * m_stageScale / bitmapRes; - } else { - m_width = m_costumeWidth * m_stageScale / bitmapRes; - m_height = m_height = m_costumeHeight * m_stageScale / bitmapRes; - } + if (m_skin && m_costume) { + Texture texture = m_skin->getTexture(m_size * m_stageScale); + m_texture = texture; + m_width = texture.width(); + m_height = texture.height(); + setScale(m_size * m_stageScale / m_skin->getTextureScale(texture) / m_costume->bitmapResolution()); } - - Q_ASSERT(m_maxSize > 0); - - if (!m_stageModel && (m_size > m_maxSize) && (m_maxSize != 0)) - setScale(m_size / m_maxSize); - else - setScale(1); } void RenderedTarget::handleSceneMouseMove(qreal x, qreal y) @@ -554,35 +576,16 @@ void RenderedTarget::handleSceneMouseMove(qreal x, qreal y) } } -QBuffer *RenderedTarget::bitmapBuffer() -{ - return &m_bitmapBuffer; -} - -const QString &RenderedTarget::bitmapUniqueKey() const -{ - return m_bitmapUniqueKey; -} - -void RenderedTarget::lockCostume() -{ - m_costumeMutex.lock(); -} - -void RenderedTarget::unlockCostume() +QPointF RenderedTarget::transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const { - m_costumeMutex.unlock(); + const double cosRot = std::cos(rot); + const double sinRot = std::sin(rot); + const double x = (scratchX - originX) * cosRot - (scratchY - originY) * sinRot; + const double y = (scratchX - originX) * sinRot + (scratchY - originY) * cosRot; + return QPointF(x, y); } bool RenderedTarget::mirrorHorizontally() const { return m_mirrorHorizontally; } - -bool RenderedTarget::isSvg() const -{ - if (!m_costume) - return false; - - return (m_costume->dataFormat() == "svg"); -} diff --git a/src/renderedtarget.h b/src/renderedtarget.h index 8cb92e9..2e0f5dc 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -9,6 +9,7 @@ #include #include "irenderedtarget.h" +#include "texture.h" Q_MOC_INCLUDE("stagemodel.h"); Q_MOC_INCLUDE("spritemodel.h"); @@ -17,6 +18,8 @@ Q_MOC_INCLUDE("scenemousearea.h"); namespace scratchcpprender { +class Skin; + class RenderedTarget : public IRenderedTarget { Q_OBJECT @@ -30,6 +33,7 @@ class RenderedTarget : public IRenderedTarget public: RenderedTarget(QNanoQuickItem *parent = nullptr); + ~RenderedTarget(); void updateVisibility(bool visible) override; void updateX(double x) override; @@ -38,8 +42,10 @@ class RenderedTarget : public IRenderedTarget void updateDirection(double direction) override; void updateRotationStyle(libscratchcpp::Sprite::RotationStyle style) override; void updateLayerOrder(int layerOrder) override; + void updateCostume(libscratchcpp::Costume *costume) override; - void loadCostume(libscratchcpp::Costume *costume) override; + bool costumesLoaded() const override; + void loadCostumes() override; void beforeRedraw() override; @@ -68,18 +74,13 @@ class RenderedTarget : public IRenderedTarget qreal height() const override; void setHeight(qreal height) override; - QPointF mapFromScene(const QPointF &point) const override; - - QBuffer *bitmapBuffer() override; - const QString &bitmapUniqueKey() const override; + libscratchcpp::Rect getBounds() const override; - void lockCostume() override; - void unlockCostume() override; + QPointF mapFromScene(const QPointF &point) const override; bool mirrorHorizontally() const override; - bool isSvg() const override; - void paintSvg(QNanoPainter *painter) override; + Texture texture() const override; void updateHullPoints(QOpenGLFramebufferObject *fbo) override; const std::vector &hullPoints() const override; @@ -105,27 +106,24 @@ class RenderedTarget : public IRenderedTarget void calculateRotation(); void calculateSize(); void handleSceneMouseMove(qreal x, qreal y); + QPointF transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const; libscratchcpp::IEngine *m_engine = nullptr; libscratchcpp::Costume *m_costume = nullptr; StageModel *m_stageModel = nullptr; SpriteModel *m_spriteModel = nullptr; SceneMouseArea *m_mouseArea = nullptr; - QSvgRenderer m_svgRenderer; - QImage m_costumeBitmap; - QBuffer m_bitmapBuffer; - QString m_bitmapUniqueKey; - QMutex m_costumeMutex; - QMutex mutex; + bool m_costumesLoaded = false; + std::unordered_map m_skins; + bool m_skinsInherited = false; + Skin *m_skin = nullptr; + Texture m_texture; + Texture m_oldTexture; double m_size = 1; - double m_clampedSize = 1; - double m_maxSize = 1; - unsigned int m_costumeWidth = 0; - unsigned int m_costumeHeight = 0; double m_x = 0; double m_y = 0; - double m_width = 0; - double m_height = 0; + double m_width = 1; + double m_height = 1; double m_direction = 90; libscratchcpp::Sprite::RotationStyle m_rotationStyle = libscratchcpp::Sprite::RotationStyle::AllAround; bool m_mirrorHorizontally = false; diff --git a/src/skin.cpp b/src/skin.cpp new file mode 100644 index 0000000..61d92cb --- /dev/null +++ b/src/skin.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include "skin.h" +#include "texture.h" + +using namespace scratchcpprender; + +Skin::Skin(libscratchcpp::Costume *costume) +{ + if (!costume) + return; +} + +Texture Skin::createAndPaintTexture(int width, int height, bool multisampled) +{ + QOpenGLContext *context = QOpenGLContext::currentContext(); + + if (!context || !context->isValid() || (width <= 0 || height <= 0)) + return Texture(); + + // Create offscreen surface + QOffscreenSurface surface; + surface.setFormat(context->format()); + surface.create(); + Q_ASSERT(surface.isValid()); + + // Save old surface + QSurface *oldSurface = context->surface(); + + // Make context active on the surface + context->makeCurrent(&surface); + + const QRectF drawRect(0, 0, width, height); + const QSize drawRectSize = drawRect.size().toSize(); + + // Create multisampled FBO (if the multisampled parameter is set) + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + + if (multisampled) + format.setSamples(16); + + QOpenGLFramebufferObject fbo(drawRectSize, format); + fbo.bind(); + + // Create paint device + QOpenGLPaintDevice device(drawRectSize); + QPainter painter(&device); + painter.beginNativePainting(); + painter.setRenderHint(QPainter::Antialiasing, false); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + + // Call the skin-specific paint method + paint(&painter); + + // Done with the painting + painter.endNativePainting(); + painter.end(); + fbo.release(); + + GLuint textureHandle; + + if (multisampled) { + // Create non-multisampled FBO (we can't take the texture from the multisampled FBO) + format.setSamples(0); + + QOpenGLFramebufferObject targetFbo(drawRectSize, format); + targetFbo.bind(); + + // Blit the multisampled FBO to target FBO + QOpenGLFramebufferObject::blitFramebuffer(&targetFbo, &fbo); + + // Take the texture (will call targetFbo.release()) + textureHandle = targetFbo.takeTexture(); + } else { + // Take the texture + textureHandle = fbo.takeTexture(); + } + + // Restore old surface + context->doneCurrent(); + + if (oldSurface) + context->makeCurrent(oldSurface); + + return Texture(textureHandle, drawRectSize); +} diff --git a/src/skin.h b/src/skin.h new file mode 100644 index 0000000..54096f7 --- /dev/null +++ b/src/skin.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include +#include +#include + +namespace libscratchcpp +{ + +class Costume; + +} + +namespace scratchcpprender +{ + +class Texture; + +class Skin +{ + public: + Skin(libscratchcpp::Costume *costume); + Skin(const Skin &) = delete; + virtual ~Skin() { } + + virtual Texture getTexture(double scale) const = 0; + virtual double getTextureScale(const Texture &texture) const = 0; + + protected: + Texture createAndPaintTexture(int width, int height, bool multisampled); + virtual void paint(QPainter *painter) = 0; +}; + +} // namespace scratchcpprender diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp index fbd5cca..9064dda 100644 --- a/src/spritemodel.cpp +++ b/src/spritemodel.cpp @@ -38,7 +38,7 @@ void SpriteModel::onCloned(libscratchcpp::Sprite *clone) void SpriteModel::onCostumeChanged(libscratchcpp::Costume *costume) { if (m_renderedTarget) - m_renderedTarget->loadCostume(costume); + m_renderedTarget->updateCostume(costume); } void SpriteModel::onVisibleChanged(bool visible) @@ -93,7 +93,7 @@ void SpriteModel::onGraphicsEffectsCleared() libscratchcpp::Rect SpriteModel::boundingRect() const { - return libscratchcpp::Rect(m_sprite->x(), m_sprite->y(), m_sprite->x(), m_sprite->y()); + return m_renderedTarget->getBounds(); } libscratchcpp::Sprite *SpriteModel::sprite() const @@ -115,4 +115,12 @@ void SpriteModel::setRenderedTarget(IRenderedTarget *newRenderedTarget) emit renderedTargetChanged(); } +SpriteModel *SpriteModel::cloneRoot() const +{ + if (m_cloneRoot == this) + return nullptr; + else + return m_cloneRoot; +} + } // namespace scratchcpprender diff --git a/src/spritemodel.h b/src/spritemodel.h index b7bb5b7..8be730b 100644 --- a/src/spritemodel.h +++ b/src/spritemodel.h @@ -49,6 +49,8 @@ class SpriteModel IRenderedTarget *renderedTarget() const; void setRenderedTarget(IRenderedTarget *newRenderedTarget); + SpriteModel *cloneRoot() const; + signals: void renderedTargetChanged(); void cloned(SpriteModel *cloneModel); diff --git a/src/stagemodel.cpp b/src/stagemodel.cpp index 14521a9..92b17b5 100644 --- a/src/stagemodel.cpp +++ b/src/stagemodel.cpp @@ -20,7 +20,7 @@ void StageModel::init(libscratchcpp::Stage *stage) void StageModel::onCostumeChanged(libscratchcpp::Costume *costume) { if (m_renderedTarget) - m_renderedTarget->loadCostume(costume); + m_renderedTarget->updateCostume(costume); } void StageModel::onTempoChanged(int tempo) @@ -47,7 +47,7 @@ void StageModel::loadCostume() { if (m_renderedTarget && m_stage) { if (m_stage) - m_renderedTarget->loadCostume(m_stage->currentCostume().get()); + m_renderedTarget->updateCostume(m_stage->currentCostume().get()); } } diff --git a/src/svgskin.cpp b/src/svgskin.cpp new file mode 100644 index 0000000..bc6576c --- /dev/null +++ b/src/svgskin.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include "svgskin.h" + +using namespace scratchcpprender; + +static const int MAX_TEXTURE_DIMENSION = 2048; +static const int INDEX_OFFSET = 8; + +SVGSkin::SVGSkin(libscratchcpp::Costume *costume, bool antialiasing) : + Skin(costume), + m_antialiasing(antialiasing) +{ + if (!costume) + return; + + // Load SVG data + m_svgRen.load(QByteArray(static_cast(costume->data()), costume->dataSize())); + + // Calculate maximum index (larger images will only be scaled up) + const QRectF viewBox = m_svgRen.viewBox(); + + if (viewBox.width() == 0 || viewBox.height() == 0) + return; + + const int i1 = std::log2(MAX_TEXTURE_DIMENSION / viewBox.width()) + INDEX_OFFSET; + const int i2 = std::log2(MAX_TEXTURE_DIMENSION / viewBox.height()) + INDEX_OFFSET; + m_maxIndex = std::min(i1, i2); + + // Create all possible textures (the 1.0 scale is stored at INDEX_OFFSET) + // TODO: Is this necessary? + for (int i = 0; i <= m_maxIndex; i++) + createScaledTexture(i); +} + +SVGSkin::~SVGSkin() +{ + for (const auto &[index, texture] : m_textures) + m_textureObjects[texture].release(); +} + +Texture SVGSkin::getTexture(double scale) const +{ + // https://github.com/scratchfoundation/scratch-render/blob/423bb700c36b8c1c0baae1e2413878a4f778849a/src/SVGSkin.js#L158-L176 + int mipLevel = std::max(std::ceil(std::log2(scale)) + INDEX_OFFSET, 0.0); + + // Limit to maximum index + mipLevel = std::min(mipLevel, m_maxIndex); + + auto it = m_textures.find(mipLevel); + + if (it == m_textures.cend()) + return const_cast(this)->createScaledTexture(mipLevel); // TODO: Remove that awful const_cast ;) + else + return m_textureObjects.at(it->second); +} + +double SVGSkin::getTextureScale(const Texture &texture) const +{ + auto it = m_textureIndexes.find(texture.handle()); + + if (it != m_textureIndexes.cend()) + return std::pow(2, it->second - INDEX_OFFSET); + + return 1; +} + +void SVGSkin::paint(QPainter *painter) +{ + const QPaintDevice *device = painter->device(); + m_svgRen.render(painter, QRectF(0, 0, device->width(), device->height())); +} + +Texture SVGSkin::createScaledTexture(int index) +{ + Q_ASSERT(m_textures.find(index) == m_textures.cend()); + auto it = m_textures.find(index); + + if (it != m_textures.cend()) + return m_textureObjects[it->second]; + + const double scale = std::pow(2, index - INDEX_OFFSET); + const QRect viewBox = m_svgRen.viewBox(); + const double width = viewBox.width() * scale; + const double height = viewBox.height() * scale; + + if (width > MAX_TEXTURE_DIMENSION || height > MAX_TEXTURE_DIMENSION) { + Q_ASSERT(false); // this shouldn't happen because indexes are limited to the max index + return Texture(); + } + + const Texture texture = createAndPaintTexture(viewBox.width() * scale, viewBox.height() * scale, m_antialiasing); + + if (texture.isValid()) { + m_textures[index] = texture.handle(); + m_textureIndexes[texture.handle()] = index; + m_textureObjects[texture.handle()] = texture; + } + + return texture; +} diff --git a/src/svgskin.h b/src/svgskin.h new file mode 100644 index 0000000..95c0768 --- /dev/null +++ b/src/svgskin.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +#include "skin.h" +#include "texture.h" + +namespace scratchcpprender +{ + +class SVGSkin : public Skin +{ + public: + SVGSkin(libscratchcpp::Costume *costume, bool antialiasing = true); + ~SVGSkin(); + + Texture getTexture(double scale) const override; + double getTextureScale(const Texture &texture) const override; + + protected: + void paint(QPainter *painter) override; + + private: + Texture createScaledTexture(int index); + + std::unordered_map m_textures; + std::unordered_map m_textureIndexes; // reverse map of m_textures + std::unordered_map m_textureObjects; + QSvgRenderer m_svgRen; + int m_maxIndex = 0; + bool m_antialiasing = false; +}; + +} // namespace scratchcpprender diff --git a/src/targetpainter.cpp b/src/targetpainter.cpp index c91c3e9..7d64645 100644 --- a/src/targetpainter.cpp +++ b/src/targetpainter.cpp @@ -1,15 +1,18 @@ // SPDX-License-Identifier: LGPL-3.0-or-later +#include #include #include #include "targetpainter.h" #include "irenderedtarget.h" #include "spritemodel.h" +#include "bitmapskin.h" using namespace scratchcpprender; -TargetPainter::TargetPainter() +TargetPainter::TargetPainter(QOpenGLFramebufferObject *fbo) : + m_fbo(fbo) { } @@ -24,42 +27,57 @@ void TargetPainter::paint(QNanoPainter *painter) "application object."); } - m_target->lockCostume(); - double width = m_target->width(); - double height = m_target->height(); + QOpenGLContext *context = QOpenGLContext::currentContext(); + Q_ASSERT(context); - if (m_target->isSvg()) - m_target->paintSvg(painter); - else { - QOpenGLContext *context = QOpenGLContext::currentContext(); - Q_ASSERT(context); + if (!context) + return; - if (!context) - return; + // Custom FBO - only used for testing + QOpenGLFramebufferObject *targetFbo = m_fbo ? m_fbo : framebufferObject(); - QOffscreenSurface surface; - surface.setFormat(context->format()); - surface.create(); - Q_ASSERT(surface.isValid()); + QOpenGLExtraFunctions glF(context); + glF.initializeOpenGLFunctions(); - QSurface *oldSurface = context->surface(); - context->makeCurrent(&surface); + // Cancel current frame because we're using a custom FBO + painter->cancelFrame(); - painter->beginFrame(width, height); - QNanoImage image = QNanoImage::fromCache(painter, m_target->bitmapBuffer(), m_target->bitmapUniqueKey()); - painter->drawImage(image, 0, 0, width, height); - painter->endFrame(); + Texture texture = m_target->texture(); - context->doneCurrent(); - context->makeCurrent(oldSurface); + if (!texture.isValid()) + return; + + // Create a FBO for the current texture + unsigned int fbo; + glF.glGenFramebuffers(1, &fbo); + glF.glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glF.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.handle(), 0); + + if (glF.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + qWarning() << "error: framebuffer incomplete (" + m_target->scratchTarget()->name() + ")"; + glF.glDeleteFramebuffers(1, &fbo); + return; } - m_target->updateHullPoints(framebufferObject()); - m_target->unlockCostume(); + // Blit the FBO to the Qt Quick FBO + glF.glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + glF.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, targetFbo->handle()); + glF.glBlitFramebuffer(0, 0, texture.width(), texture.height(), 0, 0, targetFbo->width(), targetFbo->height(), GL_COLOR_BUFFER_BIT, GL_NEAREST); + glF.glBindFramebuffer(GL_FRAMEBUFFER, targetFbo->handle()); + + glF.glDeleteFramebuffers(1, &fbo); + + m_target->updateHullPoints(targetFbo); } void TargetPainter::synchronize(QNanoQuickItem *item) { m_target = dynamic_cast(item); Q_ASSERT(m_target); + + // Render costumes into textures + if (!m_target->costumesLoaded()) { + m_target->loadCostumes(); + invalidateFramebufferObject(); + } } diff --git a/src/targetpainter.h b/src/targetpainter.h index e752f96..9c6f4b2 100644 --- a/src/targetpainter.h +++ b/src/targetpainter.h @@ -8,17 +8,19 @@ namespace scratchcpprender { class IRenderedTarget; +class Skin; class TargetPainter : public QNanoQuickItemPainter { public: - TargetPainter(); + TargetPainter(QOpenGLFramebufferObject *fbo = nullptr); ~TargetPainter(); void paint(QNanoPainter *painter) override; void synchronize(QNanoQuickItem *item) override; private: + QOpenGLFramebufferObject *m_fbo = nullptr; IRenderedTarget *m_target = nullptr; }; diff --git a/src/texture.cpp b/src/texture.cpp new file mode 100644 index 0000000..a0bf2da --- /dev/null +++ b/src/texture.cpp @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "texture.h" + +using namespace scratchcpprender; + +Texture::Texture() : + m_size(QSize(0, 0)) +{ +} + +Texture::Texture(GLuint texture, const QSize &size) : + m_handle(texture), + m_isValid(true), + m_size(size) +{ +} + +Texture::Texture(GLuint texture, int width, int height) : + Texture(texture, QSize(width, height)) +{ +} + +GLuint Texture::handle() const +{ + return m_handle; +} + +bool Texture::isValid() const +{ + return m_isValid; +} + +const QSize &Texture::size() const +{ + return m_size; +} + +int Texture::width() const +{ + return m_size.width(); +} + +int Texture::height() const +{ + return m_size.height(); +} + +QImage Texture::toImage() const +{ + if (!m_isValid) + return QImage(); + + QOpenGLContext *context = QOpenGLContext::currentContext(); + + if (!context || !context->isValid()) + return QImage(); + + QOpenGLExtraFunctions glF(context); + glF.initializeOpenGLFunctions(); + + // Create offscreen surface + QOffscreenSurface surface; + surface.setFormat(context->format()); + surface.create(); + Q_ASSERT(surface.isValid()); + + // Save old surface + QSurface *oldSurface = context->surface(); + + // Make context active on the surface + context->makeCurrent(&surface); + + const QRectF drawRect(0, 0, m_size.width(), m_size.height()); + const QSize drawRectSize = drawRect.size().toSize(); + + // Create FBO, but attach the texture to it + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + + QOpenGLFramebufferObject fbo(drawRectSize, format); + + // Create a custom FBO with the texture + unsigned int textureFbo; + glF.glGenFramebuffers(1, &textureFbo); + glF.glBindFramebuffer(GL_FRAMEBUFFER, textureFbo); + glF.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_handle, 0); + + if (glF.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + qWarning() << "error: framebuffer incomplete when generating texture image"; + glF.glDeleteFramebuffers(1, &textureFbo); + return QImage(); + } + + // Blit the FBO to the Qt FBO + glF.glBindFramebuffer(GL_READ_FRAMEBUFFER, textureFbo); + glF.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo.handle()); + glF.glBlitFramebuffer(0, 0, m_size.width(), m_size.height(), 0, 0, fbo.width(), fbo.height(), GL_COLOR_BUFFER_BIT, GL_NEAREST); + glF.glBindFramebuffer(GL_FRAMEBUFFER, 0); + + glF.glDeleteFramebuffers(1, &textureFbo); + + // Get the image + QImage image = fbo.toImage(); + + // Restore old surface + context->doneCurrent(); + + if (oldSurface) + context->makeCurrent(oldSurface); + + return image; +} + +void Texture::release() +{ + if (m_isValid) { + glDeleteTextures(1, &m_handle); + m_isValid = false; + } +} + +bool Texture::operator==(const Texture &texture) const +{ + return (!m_isValid && !texture.m_isValid) || (m_isValid && texture.m_isValid && m_handle == texture.m_handle); +} + +bool scratchcpprender::Texture::operator!=(const Texture &texture) const +{ + return !(*this == texture); +} diff --git a/src/texture.h b/src/texture.h new file mode 100644 index 0000000..894352a --- /dev/null +++ b/src/texture.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include + +namespace scratchcpprender +{ + +class Texture +{ + public: + Texture(); + Texture(GLuint texture, const QSize &size); + Texture(GLuint texture, int width, int height); + + GLuint handle() const; + bool isValid() const; + const QSize &size() const; + int width() const; + int height() const; + + QImage toImage() const; + + void release(); + + bool operator==(const Texture &texture) const; + bool operator!=(const Texture &texture) const; + + private: + GLuint m_handle = 0; + bool m_isValid = false; + QSize m_size; +}; + +} // namespace scratchcpprender diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 20be59f..7be095e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -29,3 +29,5 @@ add_subdirectory(keyeventhandler) add_subdirectory(mouseeventhandler) add_subdirectory(scenemousearea) add_subdirectory(monitor_models) +add_subdirectory(texture) +add_subdirectory(skins) diff --git a/test/jpeg_result.png b/test/jpeg_result.png new file mode 100644 index 0000000..9123a60 Binary files /dev/null and b/test/jpeg_result.png differ diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h index ab7cf2c..ba4435d 100644 --- a/test/mocks/renderedtargetmock.h +++ b/test/mocks/renderedtargetmock.h @@ -1,7 +1,9 @@ #pragma once #include +#include #include +#include #include using namespace scratchcpprender; @@ -19,10 +21,12 @@ class RenderedTargetMock : public IRenderedTarget MOCK_METHOD(void, updateDirection, (double), (override)); MOCK_METHOD(void, updateRotationStyle, (libscratchcpp::Sprite::RotationStyle), (override)); MOCK_METHOD(void, updateLayerOrder, (int), (override)); + MOCK_METHOD(void, updateCostume, (libscratchcpp::Costume *), (override)); - MOCK_METHOD(void, beforeRedraw, (), (override)); + MOCK_METHOD(bool, costumesLoaded, (), (const, override)); + MOCK_METHOD(void, loadCostumes, (), (override)); - MOCK_METHOD(void, loadCostume, (libscratchcpp::Costume *), (override)); + MOCK_METHOD(void, beforeRedraw, (), (override)); MOCK_METHOD(void, deinitClone, (), (override)); @@ -51,16 +55,11 @@ class RenderedTargetMock : public IRenderedTarget MOCK_METHOD(QPointF, mapFromScene, (const QPointF &), (const, override)); - MOCK_METHOD(QBuffer *, bitmapBuffer, (), (override)); - MOCK_METHOD(const QString &, bitmapUniqueKey, (), (const, override)); - - MOCK_METHOD(void, lockCostume, (), (override)); - MOCK_METHOD(void, unlockCostume, (), (override)); + MOCK_METHOD(libscratchcpp::Rect, getBounds, (), (const, override)); MOCK_METHOD(bool, mirrorHorizontally, (), (const, override)); - MOCK_METHOD(bool, isSvg, (), (const, override)); - MOCK_METHOD(void, paintSvg, (QNanoPainter *), (override)); + MOCK_METHOD(Texture, texture, (), (const, override)); MOCK_METHOD(void, updateHullPoints, (QOpenGLFramebufferObject *), (override)); MOCK_METHOD(const std::vector &, hullPoints, (), (const, override)); diff --git a/test/png_result.png b/test/png_result.png new file mode 100644 index 0000000..0b6a90e Binary files /dev/null and b/test/png_result.png differ diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index 46ea4a2..bba48ed 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "../common.h" @@ -50,39 +51,53 @@ TEST_F(RenderedTargetTest, Constructors) TEST_F(RenderedTargetTest, UpdateMethods) { + QOpenGLContext context; + QOffscreenSurface surface; + createContextAndSurface(&context, &surface); RenderedTarget parent; // a parent item is needed for setVisible() to work RenderedTarget target(&parent); QSignalSpy mirrorHorizontallySpy(&target, &RenderedTarget::mirrorHorizontallyChanged); + ASSERT_FALSE(target.costumesLoaded()); // Stage Stage stage; StageModel stageModel; stage.setInterface(&stageModel); target.setStageModel(&stageModel); - Costume costume("", "", ""); + auto costume = std::make_shared("", "", "png"); std::string costumeData = readFileStr("image.png"); - costume.setData(costumeData.size(), static_cast(costumeData.data())); - costume.setRotationCenterX(-23); - costume.setRotationCenterY(72); - costume.setBitmapResolution(2.5); + costume->setData(costumeData.size(), static_cast(costumeData.data())); + costume->setRotationCenterX(-23); + costume->setRotationCenterY(72); + costume->setBitmapResolution(2.5); + stage.addCostume(costume); + target.loadCostumes(); + ASSERT_TRUE(target.costumesLoaded()); EngineMock engine; target.setEngine(&engine); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); - target.loadCostume(&costume); + target.updateCostume(costume.get()); target.beforeRedraw(); - ASSERT_EQ(target.width(), 1.6); - ASSERT_EQ(target.height(), 2.4); - ASSERT_EQ(target.x(), 249.2); - ASSERT_EQ(target.y(), 151.2); + ASSERT_EQ(target.width(), 4); + ASSERT_EQ(target.height(), 6); + ASSERT_EQ(target.x(), 263); + ASSERT_EQ(target.y(), 108); ASSERT_EQ(target.z(), 0); ASSERT_EQ(target.rotation(), 0); - ASSERT_EQ(target.transformOriginPoint(), QPointF(-9.2, 28.8)); + ASSERT_EQ(target.transformOriginPoint(), QPointF(-23, 72)); + ASSERT_EQ(target.transformOrigin(), QQuickItem::Center); + ASSERT_EQ(target.scale(), 0.4); target.setStageModel(nullptr); ASSERT_TRUE(mirrorHorizontallySpy.empty()); + Texture texture = target.texture(); + ASSERT_TRUE(texture.isValid()); + ASSERT_EQ(texture.width(), 4); + ASSERT_EQ(texture.height(), 6); + // Sprite Sprite sprite; sprite.setVisible(true); @@ -92,24 +107,33 @@ TEST_F(RenderedTargetTest, UpdateMethods) sprite.setX(0); sprite.setY(0); sprite.setLayerOrder(3); + sprite.addCostume(costume); SpriteModel spriteModel; sprite.setInterface(&spriteModel); - EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); - EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + EXPECT_CALL(engine, stageWidth()).Times(2).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).Times(2).WillRepeatedly(Return(360)); target.setSpriteModel(&spriteModel); + target.loadCostumes(); target.beforeRedraw(); - ASSERT_EQ(std::round(target.width() * 100) / 100, 2.3); - ASSERT_EQ(std::round(target.height() * 100) / 100, 3.46); - ASSERT_EQ(std::round(target.x() * 100) / 100, 253.25); - ASSERT_EQ(std::round(target.y() * 100) / 100, 138.53); + ASSERT_EQ(target.width(), 4); + ASSERT_EQ(target.height(), 6); + ASSERT_EQ(target.x(), 263); + ASSERT_EQ(target.y(), 108); ASSERT_EQ(target.z(), 3); ASSERT_EQ(target.rotation(), -157.16); - ASSERT_EQ(std::round(target.transformOriginPoint().x() * 100) / 100, -13.25); - ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 41.47); + ASSERT_EQ(target.transformOriginPoint().x(), -23); + ASSERT_EQ(target.transformOriginPoint().y(), 72); + ASSERT_EQ(target.transformOrigin(), QQuickItem::Center); + ASSERT_EQ(std::round(target.scale() * 100) / 100, 0.58); ASSERT_TRUE(mirrorHorizontallySpy.empty()); + texture = target.texture(); + ASSERT_TRUE(texture.isValid()); + ASSERT_EQ(texture.width(), 4); + ASSERT_EQ(texture.height(), 6); + // Visibility target.updateVisibility(false); ASSERT_FALSE(target.isVisible()); @@ -123,27 +147,27 @@ TEST_F(RenderedTargetTest, UpdateMethods) EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.updateX(12.5); - ASSERT_EQ(std::round(target.x() * 100) / 100, 265.75); - ASSERT_EQ(std::round(target.y() * 100) / 100, 138.53); + ASSERT_EQ(target.x(), 275.5); + ASSERT_EQ(target.y(), 108); // Y EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.updateY(-76.09); - ASSERT_EQ(std::round(target.x() * 100) / 100, 265.75); - ASSERT_EQ(std::round(target.y() * 100) / 100, 214.62); + ASSERT_EQ(target.x(), 275.5); + ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09); // Size EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.updateSize(56.2); target.beforeRedraw(); - ASSERT_EQ(std::round(target.width() * 100) / 100, 0.9); - ASSERT_EQ(std::round(target.height() * 100) / 100, 1.35); - ASSERT_EQ(std::round(target.x() * 100) / 100, 257.67); - ASSERT_EQ(std::round(target.y() * 100) / 100, 239.9); - ASSERT_EQ(std::round(target.transformOriginPoint().x() * 100) / 100, -5.17); - ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 16.19); + ASSERT_EQ(target.width(), 4); + ASSERT_EQ(target.height(), 6); + ASSERT_EQ(target.x(), 275.5); + ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09); + ASSERT_EQ(target.transformOriginPoint().x(), -23); + ASSERT_EQ(target.transformOriginPoint().y(), 72); // Direction target.updateDirection(123.8); @@ -156,8 +180,8 @@ TEST_F(RenderedTargetTest, UpdateMethods) target.updateRotationStyle(Sprite::RotationStyle::LeftRight); ASSERT_EQ(target.mirrorHorizontally(), false); ASSERT_EQ(target.rotation(), 0); - ASSERT_EQ(std::round(target.x() * 100) / 100, 257.67); - ASSERT_EQ(std::round(target.y() * 100) / 100, 239.9); + ASSERT_EQ(target.x(), 275.5); + ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09); ASSERT_TRUE(mirrorHorizontallySpy.empty()); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); @@ -165,8 +189,8 @@ TEST_F(RenderedTargetTest, UpdateMethods) target.updateDirection(-15); ASSERT_EQ(target.mirrorHorizontally(), true); ASSERT_EQ(target.rotation(), 0); - ASSERT_EQ(std::round(target.x() * 100) / 100, 247.33); - ASSERT_EQ(std::round(target.y() * 100) / 100, 239.9); + ASSERT_EQ(target.x(), 229.5); + ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09); ASSERT_EQ(mirrorHorizontallySpy.count(), 1); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); @@ -174,8 +198,8 @@ TEST_F(RenderedTargetTest, UpdateMethods) target.updateDirection(134.89); ASSERT_EQ(target.mirrorHorizontally(), false); ASSERT_EQ(target.rotation(), 0); - ASSERT_EQ(std::round(target.x() * 100) / 100, 257.67); - ASSERT_EQ(std::round(target.y() * 100) / 100, 239.9); + ASSERT_EQ(target.x(), 275.5); + ASSERT_EQ(std::round(target.y() * 100) / 100, 184.09); ASSERT_EQ(mirrorHorizontallySpy.count(), 2); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); @@ -184,110 +208,69 @@ TEST_F(RenderedTargetTest, UpdateMethods) ASSERT_EQ(target.mirrorHorizontally(), false); ASSERT_EQ(target.rotation(), 0); ASSERT_EQ(mirrorHorizontallySpy.count(), 2); -} - -TEST_F(RenderedTargetTest, LoadJpegCostume) -{ - std::string str = readFileStr("image.jpg"); - Costume costume("", "abc", "jpg"); - costume.setData(str.size(), static_cast(const_cast(str.c_str()))); - costume.setBitmapResolution(3); - - RenderedTarget target; - target.loadCostume(&costume); - ASSERT_FALSE(target.isSvg()); - ASSERT_FALSE(target.bitmapBuffer()->isOpen()); - target.bitmapBuffer()->open(QBuffer::ReadOnly); - ASSERT_EQ(target.bitmapBuffer()->readAll().toStdString(), str); - ASSERT_EQ(target.bitmapUniqueKey().toStdString(), costume.id()); -} - -TEST_F(RenderedTargetTest, LoadPngCostume) -{ - std::string str = readFileStr("image.png"); - Costume costume("", "abc", "png"); - costume.setData(str.size(), static_cast(const_cast(str.c_str()))); - costume.setBitmapResolution(3); - - RenderedTarget target; - target.loadCostume(&costume); - ASSERT_FALSE(target.isSvg()); - ASSERT_FALSE(target.bitmapBuffer()->isOpen()); - target.bitmapBuffer()->open(QBuffer::ReadOnly); - ASSERT_EQ(target.bitmapBuffer()->readAll().toStdString(), str); - ASSERT_EQ(target.bitmapUniqueKey().toStdString(), costume.id()); -} - -TEST_F(RenderedTargetTest, LoadSvgCostume) -{ - // Get maximum viewport dimensions - QOpenGLContext context; - context.create(); - Q_ASSERT(context.isValid()); - - QOffscreenSurface surface; - surface.create(); - Q_ASSERT(surface.isValid()); - - context.makeCurrent(&surface); - GLint dims[2]; - glGetIntegerv(GL_MAX_VIEWPORT_DIMS, dims); - double maxWidth = dims[0] * 0.1; - double maxHeight = dims[1] * 0.1; - double maxSize = std::min(maxWidth / (1143 / 90.0), maxHeight / (1143 / 90.0)); - context.doneCurrent(); - - std::string str = readFileStr("image.svg"); - auto costume = std::make_shared("", "abc", "svg"); - costume->setData(str.size(), static_cast(const_cast(str.c_str()))); - costume->setBitmapResolution(1); - - EngineMock engine; - SpriteModel model; - Sprite sprite; - sprite.setSize(maxSize * 100); - sprite.setX(49.7); - sprite.setY(-64.15); - costume->setRotationCenterX(-84); - costume->setRotationCenterY(53); - model.init(&sprite); - - RenderedTarget target; - target.setEngine(&engine); + // Stage scale EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); - target.setSpriteModel(&model); - target.loadCostume(costume.get()); + target.setStageScale(1.5); target.beforeRedraw(); - ASSERT_TRUE(target.isSvg()); - ASSERT_FALSE(target.bitmapBuffer()->isOpen()); - target.bitmapBuffer()->open(QBuffer::ReadOnly); - ASSERT_TRUE(target.bitmapBuffer()->readAll().toStdString().empty()); - ASSERT_TRUE(target.bitmapUniqueKey().toStdString().empty()); - target.bitmapBuffer()->close(); - - ASSERT_EQ(std::round(target.width() * 100) / 100, 1548.09); - ASSERT_EQ(std::round(target.height() * 100) / 100, 1548.09); - ASSERT_EQ(target.scale(), 1); - ASSERT_EQ(std::round(target.x() * 100) / 100, 11126.36); - ASSERT_EQ(std::round(target.y() * 100) / 100, -6593.27); - ASSERT_EQ(std::round(target.transformOriginPoint().x() * 100) / 100, -10836.66); - ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 6837.42); - - // Test scale limit + ASSERT_EQ(target.width(), 4); + ASSERT_EQ(target.height(), 6); + ASSERT_EQ(target.x(), 401.75); + ASSERT_EQ(std::round(target.y() * 100) / 100, 312.14); + ASSERT_EQ(target.z(), 3); + ASSERT_EQ(target.rotation(), 0); + ASSERT_EQ(target.transformOriginPoint().x(), -23); + ASSERT_EQ(target.transformOriginPoint().y(), 72); + ASSERT_EQ(target.transformOrigin(), QQuickItem::Center); + ASSERT_EQ(std::round(target.scale() * 100) / 100, 0.34); + + // Null rotation center + costume->setRotationCenterX(0); + costume->setRotationCenterY(0); + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + target.updateSize(100); + target.beforeRedraw(); + ASSERT_EQ(target.width(), 4); + ASSERT_EQ(target.height(), 6); + ASSERT_EQ(target.x(), 378.75); + ASSERT_EQ(std::round(target.y() * 100) / 100, 384.14); + ASSERT_EQ(target.transformOriginPoint().x(), 0); + ASSERT_EQ(target.transformOriginPoint().y(), 0); + ASSERT_EQ(target.transformOrigin(), QQuickItem::TopLeft); + + // SVG + costume = std::make_shared("", "", "svg"); + std::string svgCostumeData = readFileStr("image.svg"); + costume->setData(svgCostumeData.size(), static_cast(svgCostumeData.data())); + costume->setRotationCenterX(25); + costume->setRotationCenterY(-8); + sprite.addCostume(costume); + EXPECT_CALL(engine, stageWidth()).Times(2).WillRepeatedly(Return(480)); EXPECT_CALL(engine, stageHeight()).Times(2).WillRepeatedly(Return(360)); - target.updateSize(maxSize * 250); - target.setStageScale(3.89); - - ASSERT_EQ(std::round(target.width() * 100) / 100, 1548.09); - ASSERT_EQ(std::round(target.height() * 100) / 100, 1548.09); - ASSERT_EQ(std::round(target.scale() * 100) / 100, 9.19); - ASSERT_EQ(std::round(target.x() * 100) / 100, 12595.73); - ASSERT_EQ(std::round(target.y() * 100) / 100, -6286.52); - ASSERT_EQ(std::round(target.transformOriginPoint().x() * 100) / 100, -11468.8); - ASSERT_EQ(std::round(target.transformOriginPoint().y() * 100) / 100, 7236.27); + target.loadCostumes(); + target.updateCostume(costume.get()); + target.beforeRedraw(); + + ASSERT_EQ(target.width(), 26); + ASSERT_EQ(target.height(), 26); + ASSERT_EQ(target.x(), 328.75); + ASSERT_EQ(std::round(target.y() * 100) / 100, 400.14); + ASSERT_EQ(target.z(), 3); + ASSERT_EQ(target.rotation(), 0); + ASSERT_EQ(target.transformOriginPoint().x(), 50); + ASSERT_EQ(target.transformOriginPoint().y(), -16); + ASSERT_EQ(target.transformOrigin(), QQuickItem::Center); + ASSERT_EQ(std::round(target.scale() * 100) / 100, 0.75); + + texture = target.texture(); + ASSERT_TRUE(texture.isValid()); + ASSERT_EQ(texture.width(), 26); + ASSERT_EQ(texture.height(), 26); + + context.doneCurrent(); } TEST_F(RenderedTargetTest, DeinitClone) @@ -305,61 +288,6 @@ TEST_F(RenderedTargetTest, DeinitClone) ASSERT_EQ(mouseArea.draggedSprite(), nullptr); } -TEST_F(RenderedTargetTest, PaintSvg) -{ - std::string str = readFileStr("image.svg"); - Costume costume("", "abc", "svg"); - costume.setData(str.size(), static_cast(const_cast(str.c_str()))); - costume.setBitmapResolution(3); - - EngineMock engine; - Sprite sprite; - sprite.setSize(2525.7); - - SpriteModel model; - model.init(&sprite); - - EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); - EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); - RenderedTarget target; - target.setEngine(&engine); - target.setSpriteModel(&model); - target.loadCostume(&costume); - target.beforeRedraw(); - - // Create OpenGL context - QOpenGLContext context; - QOffscreenSurface surface; - createContextAndSurface(&context, &surface); - - // Create a painter - QNanoPainter painter; - - QOpenGLFramebufferObjectFormat format; - format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); - - // Begin painting - QOpenGLFramebufferObject fbo(100, 100, format); - fbo.bind(); - painter.beginFrame(fbo.width(), fbo.height()); - - // Paint - target.paintSvg(&painter); - painter.endFrame(); - - // Compare with reference image - QBuffer buffer; - fbo.toImage().save(&buffer, "png"); - QFile ref("svg_result.png"); - ref.open(QFile::ReadOnly); - buffer.open(QBuffer::ReadOnly); - ASSERT_EQ(buffer.readAll(), ref.readAll()); - - // Release - fbo.release(); - context.doneCurrent(); -} - TEST_F(RenderedTargetTest, HullPoints) { EngineMock engine; @@ -398,19 +326,6 @@ TEST_F(RenderedTargetTest, HullPoints) target.updateHullPoints(&fbo); ASSERT_EQ(target.hullPoints(), std::vector({ { 1, 1 }, { 2, 1 }, { 3, 1 }, { 1, 2 }, { 3, 2 }, { 1, 3 }, { 2, 3 }, { 3, 3 } })); - // Begin painting (multisampled) - format.setSamples(16); - QOpenGLFramebufferObject fboMultiSampled(4, 6, format); - fboMultiSampled.bind(); - painter.beginFrame(fboMultiSampled.width(), fboMultiSampled.height()); - - // Paint (multisampled) - painter.drawImage(image, 0, 0); - painter.endFrame(); - - // Test hull points (this is undefined with multisampling, so we just check if there are any hull points) - ASSERT_FALSE(target.hullPoints().empty()); - // Release fbo.release(); context.doneCurrent(); @@ -640,3 +555,75 @@ TEST_F(RenderedTargetTest, StageScale) target.setStageScale(6.4); ASSERT_EQ(target.stageScale(), 6.4); } + +TEST_F(RenderedTargetTest, GetBounds) +{ + QOpenGLContext context; + QOffscreenSurface surface; + createContextAndSurface(&context, &surface); + QOpenGLExtraFunctions glF(&context); + glF.initializeOpenGLFunctions(); + RenderedTarget target; + + Sprite sprite; + sprite.setX(75.64); + sprite.setY(-120.3); + sprite.setDirection(-46.37); + sprite.setSize(67.98); + SpriteModel spriteModel; + sprite.setInterface(&spriteModel); + target.setSpriteModel(&spriteModel); + EngineMock engine; + target.setEngine(&engine); + auto costume = std::make_shared("", "", "png"); + std::string costumeData = readFileStr("image.png"); + costume->setData(costumeData.size(), static_cast(costumeData.data())); + costume->setRotationCenterX(-15); + costume->setRotationCenterY(48); + costume->setBitmapResolution(3.25); + sprite.addCostume(costume); + + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + target.loadCostumes(); + target.updateCostume(costume.get()); + target.beforeRedraw(); + + Texture texture = target.texture(); + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + + QOpenGLFramebufferObject fbo(texture.size(), format); + fbo.bind(); + glF.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.handle(), 0); + target.updateHullPoints(&fbo); + fbo.release(); + + Rect bounds = target.getBounds(); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 66.13); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, -124.52); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, 66.72); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -125.11); + + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + target.updateRotationStyle(Sprite::RotationStyle::LeftRight); + + bounds = target.getBounds(); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 71.87); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, -110.47); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, 72.29); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -110.89); + + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + target.setStageScale(20.75); + + bounds = target.getBounds(); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 71.87); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, -110.47); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, 72.29); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -110.89); + + context.doneCurrent(); +} diff --git a/test/skins/CMakeLists.txt b/test/skins/CMakeLists.txt new file mode 100644 index 0000000..34f8ece --- /dev/null +++ b/test/skins/CMakeLists.txt @@ -0,0 +1,31 @@ +# bitmapskin +add_executable( + bitmapskin_test + bitmapskin_test.cpp +) + +target_link_libraries( + bitmapskin_test + GTest::gtest_main + scratchcpp-render + ${QT_LIBS} +) + +add_test(bitmapskin_test) +gtest_discover_tests(bitmapskin_test) + +# svgskin +add_executable( + svgskin_test + svgskin_test.cpp +) + +target_link_libraries( + svgskin_test + GTest::gtest_main + scratchcpp-render + ${QT_LIBS} +) + +add_test(svgskin_test) +gtest_discover_tests(svgskin_test) diff --git a/test/skins/bitmapskin_test.cpp b/test/skins/bitmapskin_test.cpp new file mode 100644 index 0000000..ab6eb17 --- /dev/null +++ b/test/skins/bitmapskin_test.cpp @@ -0,0 +1,74 @@ +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +class BitmapSkinTest : public testing::Test +{ + public: + void SetUp() override + { + m_context.create(); + ASSERT_TRUE(m_context.isValid()); + + m_surface.setFormat(m_context.format()); + m_surface.create(); + Q_ASSERT(m_surface.isValid()); + m_context.makeCurrent(&m_surface); + + Costume jpegCostume("", "", ""); + std::string costumeData = readFileStr("image.jpg"); + jpegCostume.setData(costumeData.size(), costumeData.data()); + m_jpegSkin = std::make_unique(&jpegCostume); + + Costume pngCostume("", "", ""); + costumeData = readFileStr("image.png"); + pngCostume.setData(costumeData.size(), costumeData.data()); + m_pngSkin = std::make_unique(&pngCostume); + } + + void TearDown() override + { + ASSERT_EQ(m_context.surface(), &m_surface); + m_context.doneCurrent(); + } + + QOpenGLContext m_context; + QOffscreenSurface m_surface; + std::unique_ptr m_jpegSkin; + std::unique_ptr m_pngSkin; +}; + +TEST_F(BitmapSkinTest, GetTexture) +{ + Texture texture = m_jpegSkin->getTexture(1); + ASSERT_EQ(texture.width(), 4); + ASSERT_EQ(texture.height(), 6); + + QBuffer jpegBuffer; + texture.toImage().save(&jpegBuffer, "png"); + QFile jpegRef("jpeg_result.png"); + jpegRef.open(QFile::ReadOnly); + jpegBuffer.open(QBuffer::ReadOnly); + ASSERT_EQ(jpegBuffer.readAll(), jpegRef.readAll()); + + texture = m_pngSkin->getTexture(1); + ASSERT_EQ(texture.width(), 4); + ASSERT_EQ(texture.height(), 6); + + QBuffer pngBuffer; + texture.toImage().save(&pngBuffer, "png"); + QFile pngRef("png_result.png"); + pngRef.open(QFile::ReadOnly); + pngBuffer.open(QBuffer::ReadOnly); + ASSERT_EQ(pngBuffer.readAll(), pngRef.readAll()); +} + +TEST_F(BitmapSkinTest, GetTextureScale) +{ + ASSERT_EQ(m_jpegSkin->getTextureScale(Texture()), 1); + ASSERT_EQ(m_pngSkin->getTextureScale(Texture()), 1); +} diff --git a/test/skins/svgskin_test.cpp b/test/skins/svgskin_test.cpp new file mode 100644 index 0000000..d028d38 --- /dev/null +++ b/test/skins/svgskin_test.cpp @@ -0,0 +1,73 @@ +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; +using namespace libscratchcpp; + +class SVGSkinTest : public testing::Test +{ + public: + void SetUp() override + { + m_context.create(); + ASSERT_TRUE(m_context.isValid()); + + m_surface.setFormat(m_context.format()); + m_surface.create(); + Q_ASSERT(m_surface.isValid()); + m_context.makeCurrent(&m_surface); + + Costume costume("", "", ""); + std::string costumeData = readFileStr("image.svg"); + costume.setData(costumeData.size(), costumeData.data()); + m_skin = std::make_unique(&costume, false); + } + + void TearDown() override + { + ASSERT_EQ(m_context.surface(), &m_surface); + m_context.doneCurrent(); + } + + QOpenGLContext m_context; + QOffscreenSurface m_surface; + std::unique_ptr m_skin; +}; + +TEST_F(SVGSkinTest, Textures) +{ + static const int INDEX_OFFSET = 8; + + for (int i = 0; i <= 18; i++) { + double scale = std::pow(2, i - INDEX_OFFSET); + Texture texture = m_skin->getTexture(scale); + int dimension = static_cast(13 * scale); + ASSERT_TRUE(texture.isValid() || dimension == 0); + + if (!texture.isValid()) + continue; + + if (i > 15) { + ASSERT_EQ(texture.width(), 1664); + ASSERT_EQ(texture.height(), 1664); + ASSERT_EQ(m_skin->getTextureScale(texture), 128); + } else { + ASSERT_EQ(texture.width(), dimension); + ASSERT_EQ(texture.height(), dimension); + ASSERT_EQ(m_skin->getTextureScale(texture), scale); + } + + // Skip images 12, 13 and 15 because they're different on xvfb + if (i == 12 || i == 13 || i >= 15) + continue; + + QBuffer buffer; + texture.toImage().save(&buffer, "png"); + QFile ref("svg_texture_results/" + QString::number(std::min(i, 15)) + ".png"); + ref.open(QFile::ReadOnly); + buffer.open(QBuffer::ReadOnly); + ASSERT_EQ(buffer.readAll(), ref.readAll()); + } +} diff --git a/test/svg_result.png b/test/svg_result.png deleted file mode 100644 index 86dceff..0000000 Binary files a/test/svg_result.png and /dev/null differ diff --git a/test/svg_texture_results/10.png b/test/svg_texture_results/10.png new file mode 100644 index 0000000..01436ab Binary files /dev/null and b/test/svg_texture_results/10.png differ diff --git a/test/svg_texture_results/11.png b/test/svg_texture_results/11.png new file mode 100644 index 0000000..73db02b Binary files /dev/null and b/test/svg_texture_results/11.png differ diff --git a/test/svg_texture_results/14.png b/test/svg_texture_results/14.png new file mode 100644 index 0000000..40bb626 Binary files /dev/null and b/test/svg_texture_results/14.png differ diff --git a/test/svg_texture_results/5.png b/test/svg_texture_results/5.png new file mode 100644 index 0000000..b0c553b Binary files /dev/null and b/test/svg_texture_results/5.png differ diff --git a/test/svg_texture_results/6.png b/test/svg_texture_results/6.png new file mode 100644 index 0000000..dddac55 Binary files /dev/null and b/test/svg_texture_results/6.png differ diff --git a/test/svg_texture_results/7.png b/test/svg_texture_results/7.png new file mode 100644 index 0000000..5c80b87 Binary files /dev/null and b/test/svg_texture_results/7.png differ diff --git a/test/svg_texture_results/8.png b/test/svg_texture_results/8.png new file mode 100644 index 0000000..bb6c391 Binary files /dev/null and b/test/svg_texture_results/8.png differ diff --git a/test/svg_texture_results/9.png b/test/svg_texture_results/9.png new file mode 100644 index 0000000..acc5697 Binary files /dev/null and b/test/svg_texture_results/9.png differ diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp index 17e9df2..f5527d1 100644 --- a/test/target_models/spritemodel_test.cpp +++ b/test/target_models/spritemodel_test.cpp @@ -8,6 +8,8 @@ using namespace scratchcpprender; using namespace libscratchcpp; +using ::testing::Return; + TEST(SpriteModelTest, Constructors) { SpriteModel model1; @@ -40,11 +42,13 @@ TEST(SpriteModelTest, DeInitClone) TEST(SpriteModelTest, OnCloned) { SpriteModel model; + ASSERT_EQ(model.cloneRoot(), nullptr); Sprite clone1; QSignalSpy spy1(&model, &SpriteModel::cloned); model.onCloned(&clone1); ASSERT_EQ(spy1.count(), 1); + ASSERT_EQ(model.cloneRoot(), nullptr); QList args = spy1.takeFirst(); ASSERT_EQ(args.size(), 1); @@ -52,6 +56,7 @@ TEST(SpriteModelTest, OnCloned) ASSERT_TRUE(cloneModel); ASSERT_EQ(cloneModel->parent(), &model); ASSERT_EQ(cloneModel->sprite(), &clone1); + ASSERT_EQ(cloneModel->cloneRoot(), &model); spy1.clear(); Sprite clone2; @@ -64,6 +69,7 @@ TEST(SpriteModelTest, OnCloned) ASSERT_TRUE(cloneModel); ASSERT_EQ(cloneModel->parent(), &model); ASSERT_EQ(cloneModel->sprite(), &clone2); + ASSERT_EQ(cloneModel->cloneRoot(), &model); Sprite clone3; QSignalSpy spy2(cloneModel, &SpriteModel::cloned); @@ -76,6 +82,7 @@ TEST(SpriteModelTest, OnCloned) ASSERT_TRUE(cloneModel); ASSERT_EQ(cloneModel->parent(), &model); ASSERT_EQ(cloneModel->sprite(), &clone3); + ASSERT_EQ(cloneModel->cloneRoot(), &model); } TEST(SpriteModelTest, OnCostumeChanged) @@ -87,7 +94,7 @@ TEST(SpriteModelTest, OnCostumeChanged) RenderedTargetMock renderedTarget; model.setRenderedTarget(&renderedTarget); - EXPECT_CALL(renderedTarget, loadCostume(&costume)); + EXPECT_CALL(renderedTarget, updateCostume(&costume)); model.onCostumeChanged(&costume); } @@ -170,6 +177,22 @@ TEST(SpriteModelTest, OnLayerOrderChanged) model.onLayerOrderChanged(7); } +TEST(SpriteModelTest, BoundingRect) +{ + SpriteModel model; + + RenderedTargetMock renderedTarget; + model.setRenderedTarget(&renderedTarget); + + Rect rect(-1, 1, 1, -1); + EXPECT_CALL(renderedTarget, getBounds()).WillOnce(Return(rect)); + Rect bounds = model.boundingRect(); + ASSERT_EQ(bounds.left(), rect.left()); + ASSERT_EQ(bounds.top(), rect.top()); + ASSERT_EQ(bounds.right(), rect.right()); + ASSERT_EQ(bounds.bottom(), rect.bottom()); +} + TEST(SpriteModelTest, RenderedTarget) { SpriteModel model; diff --git a/test/target_models/stagemodel_test.cpp b/test/target_models/stagemodel_test.cpp index 822e344..a789a5e 100644 --- a/test/target_models/stagemodel_test.cpp +++ b/test/target_models/stagemodel_test.cpp @@ -34,7 +34,7 @@ TEST(StageModelTest, OnCostumeChanged) RenderedTargetMock renderedTarget; model.setRenderedTarget(&renderedTarget); - EXPECT_CALL(renderedTarget, loadCostume(&costume)); + EXPECT_CALL(renderedTarget, updateCostume(&costume)); model.onCostumeChanged(&costume); } @@ -55,12 +55,12 @@ TEST(StageModelTest, RenderedTarget) RenderedTargetMock renderedTarget; QSignalSpy spy(&model, &StageModel::renderedTargetChanged); - EXPECT_CALL(renderedTarget, loadCostume(c2.get())); + EXPECT_CALL(renderedTarget, updateCostume(c2.get())); model.setRenderedTarget(&renderedTarget); ASSERT_EQ(spy.count(), 1); ASSERT_EQ(model.renderedTarget(), &renderedTarget); - EXPECT_CALL(renderedTarget, loadCostume(c3.get())); + EXPECT_CALL(renderedTarget, updateCostume(c3.get())); stage.setCostumeIndex(2); model.loadCostume(); } diff --git a/test/targetpainter/targetpainter_test.cpp b/test/targetpainter/targetpainter_test.cpp index 842b9bc..a33b42a 100644 --- a/test/targetpainter/targetpainter_test.cpp +++ b/test/targetpainter/targetpainter_test.cpp @@ -30,90 +30,63 @@ class TargetPainterTest : public testing::Test } }; -TEST_F(TargetPainterTest, PaintBitmap) +TEST_F(TargetPainterTest, Paint) { QOpenGLContext context; QOffscreenSurface surface; createContextAndSurface(&context, &surface); - std::unordered_map files = { { "image.jpg", "jpeg_result.png" }, { "image.png", "png_result.png" } }; - - for (const auto &[inFile, outFile] : files) { - // Create target painter - TargetPainter targetPainter; - QNanoPainter painter; - RenderedTargetMock target; - targetPainter.synchronize(&target); - - // Load the image - QBuffer buffer; - buffer.open(QBuffer::WriteOnly); - std::string str = readFileStr(inFile); - buffer.write(str.c_str(), str.size()); - buffer.close(); - - QOpenGLFramebufferObjectFormat format; - format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); - - // Begin painting reference - QNanoPainter refPainter; - QOpenGLFramebufferObject refFbo(40, 60, format); - refFbo.bind(); - refPainter.beginFrame(refFbo.width(), refFbo.height()); - - // Paint reference - QNanoImage refImage = QNanoImage::fromCache(&refPainter, &buffer, "abc"); - refPainter.drawImage(refImage, 0, 0, 40, 60); - refPainter.endFrame(); - - // Begin painting - QOpenGLFramebufferObject fbo(40, 60, format); - fbo.bind(); - painter.beginFrame(fbo.width(), fbo.height()); - - // Paint - EXPECT_CALL(target, lockCostume()); - EXPECT_CALL(target, width()).WillOnce(Return(40)); - EXPECT_CALL(target, height()).WillOnce(Return(60)); - EXPECT_CALL(target, isSvg()).WillOnce(Return(false)); - EXPECT_CALL(target, bitmapBuffer()).WillOnce(Return(&buffer)); - static const QString uniqueKey("abc"); - EXPECT_CALL(target, bitmapUniqueKey()).WillOnce(ReturnRef(uniqueKey)); - EXPECT_CALL(target, updateHullPoints); - EXPECT_CALL(target, unlockCostume()); - targetPainter.paint(&painter); - painter.endFrame(); - - // Compare resulting images - ASSERT_EQ(fbo.toImage(), refFbo.toImage()); - - // Release - fbo.release(); - refFbo.release(); - } + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); - context.doneCurrent(); -} + // Begin painting reference + QNanoPainter refPainter; + QOpenGLFramebufferObject refFbo(40, 60, format); + refFbo.bind(); + refPainter.beginFrame(refFbo.width(), refFbo.height()); -TEST_F(TargetPainterTest, PaintSvg) -{ - QOpenGLContext context; - QOffscreenSurface surface; - createContextAndSurface(&context, &surface); + // Paint reference + refPainter.setAntialias(0); + refPainter.setStrokeStyle(QNanoColor(255, 0, 0, 128)); + refPainter.ellipse(refFbo.width() / 2, refFbo.height() / 2, refFbo.width() / 2, refFbo.height() / 2); + refPainter.stroke(); + refPainter.endFrame(); - TargetPainter targetPainter; + // Begin painting QNanoPainter painter; + QOpenGLFramebufferObject fbo(40, 60, format); + fbo.bind(); + painter.beginFrame(fbo.width(), fbo.height()); + + // Create target painter + TargetPainter targetPainter(&fbo); RenderedTargetMock target; + + EXPECT_CALL(target, costumesLoaded()).WillOnce(Return(false)); + EXPECT_CALL(target, loadCostumes()); + targetPainter.synchronize(&target); + + EXPECT_CALL(target, costumesLoaded()).WillOnce(Return(true)); + EXPECT_CALL(target, loadCostumes()).Times(0); targetPainter.synchronize(&target); - EXPECT_CALL(target, lockCostume()); - EXPECT_CALL(target, width()).WillOnce(Return(40)); - EXPECT_CALL(target, height()).WillOnce(Return(60)); - EXPECT_CALL(target, isSvg()).WillOnce(Return(true)); - EXPECT_CALL(target, paintSvg(&painter)); - EXPECT_CALL(target, updateHullPoints); - EXPECT_CALL(target, unlockCostume()); + EXPECT_CALL(target, costumesLoaded()).WillOnce(Return(false)); + EXPECT_CALL(target, loadCostumes()); + targetPainter.synchronize(&target); + + // Paint + Texture texture(refFbo.texture(), refFbo.size()); + EXPECT_CALL(target, texture()).WillOnce(Return(texture)); + EXPECT_CALL(target, updateHullPoints(&fbo)); targetPainter.paint(&painter); + painter.endFrame(); + + // Compare resulting images + ASSERT_EQ(fbo.toImage(), refFbo.toImage()); + + // Release + fbo.release(); + refFbo.release(); context.doneCurrent(); } diff --git a/test/texture/CMakeLists.txt b/test/texture/CMakeLists.txt new file mode 100644 index 0000000..18d74a5 --- /dev/null +++ b/test/texture/CMakeLists.txt @@ -0,0 +1,14 @@ +add_executable( + texture_test + texture_test.cpp +) + +target_link_libraries( + texture_test + GTest::gtest_main + scratchcpp-render + ${QT_LIBS} +) + +add_test(texture_test) +gtest_discover_tests(texture_test) diff --git a/test/texture/texture_test.cpp b/test/texture/texture_test.cpp new file mode 100644 index 0000000..ffdff19 --- /dev/null +++ b/test/texture/texture_test.cpp @@ -0,0 +1,121 @@ +#include + +#include "../common.h" + +using namespace scratchcpprender; + +TEST(TextureTest, Constructors) +{ + { + Texture tex; + ASSERT_EQ(tex.handle(), 0); + ASSERT_FALSE(tex.isValid()); + ASSERT_EQ(tex.size().width(), 0); + ASSERT_EQ(tex.size().height(), 0); + ASSERT_EQ(tex.width(), 0); + ASSERT_EQ(tex.height(), 0); + } + + { + Texture tex(2, QSize(4, 2)); + ASSERT_EQ(tex.handle(), 2); + ASSERT_TRUE(tex.isValid()); + ASSERT_EQ(tex.size().width(), 4); + ASSERT_EQ(tex.size().height(), 2); + ASSERT_EQ(tex.width(), 4); + ASSERT_EQ(tex.height(), 2); + } + + { + Texture tex(2, 5, 8); + ASSERT_EQ(tex.handle(), 2); + ASSERT_TRUE(tex.isValid()); + ASSERT_EQ(tex.size().width(), 5); + ASSERT_EQ(tex.size().height(), 8); + ASSERT_EQ(tex.width(), 5); + ASSERT_EQ(tex.height(), 8); + } +} + +TEST(TextureTest, ToImage) +{ + QOpenGLContext context; + context.create(); + ASSERT_TRUE(context.isValid()); + + QOffscreenSurface surface; + surface.setFormat(context.format()); + surface.create(); + Q_ASSERT(surface.isValid()); + context.makeCurrent(&surface); + + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + + QOpenGLFramebufferObject fbo(80, 60, format); + fbo.bind(); + + QOpenGLPaintDevice device(fbo.size()); + QPainter painter(&device); + painter.beginNativePainting(); + painter.setRenderHint(QPainter::Antialiasing, false); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + painter.drawEllipse(0, 0, fbo.width(), fbo.height()); + painter.endNativePainting(); + painter.end(); + + QImage image = fbo.toImage(); + + Texture tex(fbo.takeTexture(), fbo.width(), fbo.height()); + ASSERT_EQ(tex.toImage(), image); + + tex.release(); + context.doneCurrent(); +} + +TEST(TextureTest, Release) +{ + QOpenGLContext context; + context.create(); + ASSERT_TRUE(context.isValid()); + + QOffscreenSurface surface; + surface.setFormat(context.format()); + surface.create(); + Q_ASSERT(surface.isValid()); + context.makeCurrent(&surface); + + QOpenGLFramebufferObject fbo(1, 1); + GLuint handle = fbo.takeTexture(); + ASSERT_TRUE(glIsTexture(handle)); + + Texture tex(handle, fbo.width(), fbo.height()); + ASSERT_TRUE(glIsTexture(handle)); + + tex.release(); + ASSERT_FALSE(glIsTexture(handle)); + ASSERT_FALSE(tex.isValid()); + + context.doneCurrent(); +} + +TEST(TextureTest, Operators) +{ + Texture t1; + Texture t2; + ASSERT_TRUE(t1 == t2); + ASSERT_FALSE(t1 != t2); + + Texture t3(3, 10, 10); + ASSERT_FALSE(t1 == t3); + ASSERT_TRUE(t1 != t3); + + Texture t4(3, 10, 10); + ASSERT_TRUE(t3 == t4); + ASSERT_FALSE(t3 != t4); + + Texture t5(2, 10, 10); + ASSERT_FALSE(t4 == t5); + ASSERT_TRUE(t4 != t5); +} diff --git a/thirdparty/libqnanopainter/qnanoquickitempainter.cpp b/thirdparty/libqnanopainter/qnanoquickitempainter.cpp index a2041dd..170e344 100644 --- a/thirdparty/libqnanopainter/qnanoquickitempainter.cpp +++ b/thirdparty/libqnanopainter/qnanoquickitempainter.cpp @@ -154,7 +154,6 @@ QOpenGLFramebufferObject *QNanoQuickItemPainter::createFramebufferObject(const Q { QOpenGLFramebufferObjectFormat format; format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); - format.setSamples(16); QSize fboSize(size); if (m_textureWidth > -1) fboSize.setWidth(static_cast(m_textureWidth*m_itemData.devicePixelRatio)); if (m_textureHeight > -1) fboSize.setHeight(static_cast(m_textureHeight*m_itemData.devicePixelRatio));