diff --git a/libscratchcpp b/libscratchcpp index 9fbd24f..9a1d262 160000 --- a/libscratchcpp +++ b/libscratchcpp @@ -1 +1 @@ -Subproject commit 9fbd24f1abc9a225db8e73088a85739fe8a8dfd0 +Subproject commit 9a1d262bfcf778ddb2b24f339e9aa5ed95ba8ad0 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 48d5064..fdf033a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -70,6 +70,8 @@ qt_add_qml_module(scratchcpp-render textbubbleshape.h textbubblepainter.cpp textbubblepainter.h + cputexturemanager.cpp + cputexturemanager.h blocks/penextension.cpp blocks/penextension.h blocks/penblocks.cpp diff --git a/src/ProjectPlayer.qml b/src/ProjectPlayer.qml index 4e168d9..79bf270 100644 --- a/src/ProjectPlayer.qml +++ b/src/ProjectPlayer.qml @@ -156,6 +156,53 @@ ProjectScene { } } + // Uncomment to display sprite bounding boxes (for debugging) + /*Rectangle { + function translateX(x) { + // Translates Scratch X-coordinate to the scene coordinate system + return root.stageScale * (root.stageWidth / 2 + x) + } + + function translateY(y) { + // Translates Scratch Y-coordinate to the scene coordinate system + return root.stageScale * (root.stageHeight / 2 - y) + } + + id: boundRect + color: "transparent" + border.color: "red" + border.width: 3 + visible: targetItem.visible + + function updatePosition() { + let bounds = targetItem.getQmlBounds(); + boundRect.x = translateX(bounds.left); + boundRect.y = translateY(bounds.top); + width = bounds.width * root.stageScale; + height = -bounds.height * root.stageScale; + } + + Connections { + target: targetItem + + function onXChanged() { boundRect.updatePosition() } + function onYChanged() { boundRect.updatePosition() } + function onRotationChanged() { boundRect.updatePosition() } + function onWidthChanged() { boundRect.updatePosition() } + function onHeightChanged() { boundRect.updatePosition() } + function onScaleChanged() { boundRect.updatePosition() } + } + + Connections { + property Scale transform: Scale {} + target: transform + + function onXScaleChanged() { boundRect.updatePosition() } + + Component.onCompleted: transform = targetItem.transform[0] + } + }*/ + Loader { readonly property alias model: targetItem.spriteModel active: model ? model.bubbleText !== "" : false diff --git a/src/cputexturemanager.cpp b/src/cputexturemanager.cpp new file mode 100644 index 0000000..963b0d1 --- /dev/null +++ b/src/cputexturemanager.cpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "cputexturemanager.h" +#include "texture.h" + +using namespace scratchcpprender; + +CpuTextureManager::CpuTextureManager() +{ +} + +CpuTextureManager::~CpuTextureManager() +{ + for (const auto &[handle, data] : m_textureData) + delete[] data; +} + +GLubyte *CpuTextureManager::getTextureData(const Texture &texture) +{ + if (!texture.isValid()) + return nullptr; + + const GLuint handle = texture.handle(); + auto it = m_textureData.find(handle); + + if (it == m_textureData.cend()) { + if (addTexture(texture)) + return m_textureData[handle]; + else + return nullptr; + } else + return it->second; +} + +const std::vector &CpuTextureManager::getTextureConvexHullPoints(const Texture &texture) +{ + static const std::vector empty; + + if (!texture.isValid()) + return empty; + + const GLuint handle = texture.handle(); + auto it = m_convexHullPoints.find(handle); + + if (it == m_convexHullPoints.cend()) { + if (addTexture(texture)) + return m_convexHullPoints[handle]; + else + return empty; + } else + return it->second; +} + +bool CpuTextureManager::addTexture(const Texture &texture) +{ + if (!texture.isValid()) + return false; + + const GLuint handle = texture.handle(); + const int width = texture.width(); + const int height = texture.height(); + + QOpenGLFunctions glF; + glF.initializeOpenGLFunctions(); + + // Create a FBO for the texture + unsigned int fbo; + glF.glGenFramebuffers(1, &fbo); + glF.glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glF.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, handle, 0); + + if (glF.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + qWarning() << "error: framebuffer incomplete (CpuTextureManager)"; + glF.glDeleteFramebuffers(1, &fbo); + return false; + } + + // Read pixels + GLubyte *pixels = new GLubyte[width * height * 4]; // 4 channels (RGBA) + glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + + // Flip vertically + int rowSize = width * 4; + GLubyte *tempRow = new GLubyte[rowSize]; + + for (size_t i = 0; i < height / 2; ++i) { + size_t topRowIndex = i * rowSize; + size_t bottomRowIndex = (height - 1 - i) * rowSize; + + // Swap rows + memcpy(tempRow, &pixels[topRowIndex], rowSize); + memcpy(&pixels[topRowIndex], &pixels[bottomRowIndex], rowSize); + memcpy(&pixels[bottomRowIndex], tempRow, rowSize); + } + + delete[] tempRow; + + m_textureData[handle] = pixels; + m_convexHullPoints[handle] = {}; + std::vector &hullPoints = m_convexHullPoints[handle]; + + // Get convex hull points + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int index = (y * width + x) * 4; // 4 channels (RGBA) + + // Check alpha channel + if (pixels[index + 3] > 0) + hullPoints.push_back(QPoint(x, y)); + } + } + + // Cleanup + glF.glBindFramebuffer(GL_FRAMEBUFFER, 0); + glF.glDeleteFramebuffers(1, &fbo); + + return true; +} diff --git a/src/cputexturemanager.h b/src/cputexturemanager.h new file mode 100644 index 0000000..6b5cace --- /dev/null +++ b/src/cputexturemanager.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#pragma once + +#include +#include +#include + +namespace scratchcpprender +{ + +class Texture; + +class CpuTextureManager +{ + public: + CpuTextureManager(); + ~CpuTextureManager(); + + GLubyte *getTextureData(const Texture &texture); + const std::vector &getTextureConvexHullPoints(const Texture &texture); + + private: + bool addTexture(const Texture &texture); + + std::unordered_map m_textureData; + std::unordered_map> m_convexHullPoints; +}; + +} // namespace scratchcpprender diff --git a/src/internal/TextBubble.qml b/src/internal/TextBubble.qml index 8a77084..f70df26 100644 --- a/src/internal/TextBubble.qml +++ b/src/internal/TextBubble.qml @@ -85,6 +85,15 @@ TextBubbleShape { function onScaleChanged() { positionBubble() } } + Connections { + property Scale transform: Scale {} + target: transform + + function onXScaleChanged() { positionBubble() } + + Component.onCompleted: transform = root.target.transform[0] + } + Text { id: bubbleText anchors.left: parent.left diff --git a/src/irenderedtarget.h b/src/irenderedtarget.h index 5c26352..1a5f267 100644 --- a/src/irenderedtarget.h +++ b/src/irenderedtarget.h @@ -23,7 +23,7 @@ class Texture; class IRenderedTarget : public QNanoQuickItem { public: - IRenderedTarget(QNanoQuickItem *parent = nullptr) : + IRenderedTarget(QQuickItem *parent = nullptr) : QNanoQuickItem(parent) { } @@ -83,8 +83,11 @@ class IRenderedTarget : public QNanoQuickItem virtual void setGraphicEffect(ShaderManager::Effect effect, double value) = 0; virtual void clearGraphicEffects() = 0; - virtual void updateHullPoints(QOpenGLFramebufferObject *fbo) = 0; - virtual const std::vector &hullPoints() const = 0; + virtual const std::vector &hullPoints() const = 0; + + virtual bool containsScratchPoint(double x, double y) const = 0; + + virtual bool touchingClones(const std::vector &clones) const = 0; }; } // namespace scratchcpprender diff --git a/src/projectloader.cpp b/src/projectloader.cpp index 84a7544..cfe95be 100644 --- a/src/projectloader.cpp +++ b/src/projectloader.cpp @@ -20,7 +20,7 @@ using namespace libscratchcpp; ProjectLoader::ProjectLoader(QObject *parent) : QObject(parent) { - m_project.setDownloadProgressCallback([this](unsigned int finished, unsigned int all) { + m_project.downloadProgressChanged().connect([this](unsigned int finished, unsigned int all) { if (finished != m_downloadedAssets) { m_downloadedAssets = finished; emit downloadedAssetsChanged(); @@ -208,12 +208,8 @@ void ProjectLoader::stop() void ProjectLoader::answerQuestion(const QString &answer) { - if (m_engine) { - auto f = m_engine->questionAnswered(); - - if (f) - f(answer.toStdString()); - } + if (m_engine) + m_engine->questionAnswered()(answer.toStdString()); } void ProjectLoader::timerEvent(QTimerEvent *event) @@ -255,16 +251,11 @@ void ProjectLoader::load() m_engine->setCloneLimit(m_cloneLimit); m_engine->setSpriteFencingEnabled(m_spriteFencing); - auto redrawHandler = std::bind(&ProjectLoader::redraw, this); - m_engine->setRedrawHandler(std::function(redrawHandler)); - - auto addMonitorHandler = std::bind(&ProjectLoader::addMonitor, this, std::placeholders::_1); - m_engine->setAddMonitorHandler(std::function(addMonitorHandler)); - - auto removeMonitorHandler = std::bind(&ProjectLoader::removeMonitor, this, std::placeholders::_1, std::placeholders::_2); - m_engine->setRemoveMonitorHandler(std::function(removeMonitorHandler)); + m_engine->aboutToRender().connect(&ProjectLoader::redraw, this); + m_engine->monitorAdded().connect(&ProjectLoader::addMonitor, this); + m_engine->monitorRemoved().connect(&ProjectLoader::removeMonitor, this); - m_engine->setQuestionAsked([this](const std::string &question) { emit questionAsked(QString::fromStdString(question)); }); + m_engine->questionAsked().connect([this](const std::string &question) { emit questionAsked(QString::fromStdString(question)); }); // Load targets const auto &targets = m_engine->targets(); diff --git a/src/renderedtarget.cpp b/src/renderedtarget.cpp index 20262ce..6c33896 100644 --- a/src/renderedtarget.cpp +++ b/src/renderedtarget.cpp @@ -13,6 +13,7 @@ #include "scenemousearea.h" #include "bitmapskin.h" #include "svgskin.h" +#include "cputexturemanager.h" using namespace scratchcpprender; using namespace libscratchcpp; @@ -20,7 +21,7 @@ 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) : +RenderedTarget::RenderedTarget(QQuickItem *parent) : IRenderedTarget(parent) { setSmooth(false); @@ -41,6 +42,7 @@ void RenderedTarget::updateVisibility(bool visible) setVisible(visible); calculatePos(); + m_convexHullDirty = true; } void RenderedTarget::updateX(double x) @@ -212,6 +214,8 @@ void RenderedTarget::setEngine(IEngine *newEngine) m_skin = nullptr; m_texture = Texture(); m_oldTexture = Texture(); + m_cpuTexture = Texture(); + m_convexHullDirty = true; clearGraphicEffects(); m_hullPoints.clear(); @@ -256,9 +260,12 @@ void RenderedTarget::setSpriteModel(SpriteModel *newSpriteModel) SpriteModel *cloneRoot = m_spriteModel->cloneRoot(); if (cloneRoot) { - // Inherit skins from the clone root + // Inherit skins, texture mananger, convex hull points, etc. from the clone root RenderedTarget *target = dynamic_cast(cloneRoot->renderedTarget()); Q_ASSERT(target); + m_textureManager = target->m_textureManager; + m_convexHullDirty = target->m_convexHullDirty; + m_hullPoints = target->m_hullPoints; if (target->costumesLoaded()) { m_skins = target->m_skins; // TODO: Avoid copying - maybe using a pointer? @@ -352,23 +359,27 @@ void RenderedTarget::setHeight(qreal 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()) + if (!m_costume || !m_skin || !m_texture.isValid() || !m_cpuTexture.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 width = m_cpuTexture.width() * m_size / m_costume->bitmapResolution(); + const double height = m_cpuTexture.height() * m_size / m_costume->bitmapResolution(); + const double originX = m_costume->rotationCenterX() * m_size / m_costume->bitmapResolution() - width / 2; + const double originY = -m_costume->rotationCenterY() * m_size / m_costume->bitmapResolution() + height / 2; const double rot = -rotation() * pi / 180; + const double sinRot = std::sin(rot); + const double cosRot = std::cos(rot); 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; + const std::vector &points = hullPoints(); + + for (const QPointF &point : points) { + QPointF transformed = transformPoint(point.x() - width / 2, height / 2 - point.y(), originX, originY, sinRot, cosRot); + const double x = transformed.x() * (m_mirrorHorizontally ? -1 : 1); + const double y = transformed.y(); if (x < left) left = x; @@ -386,6 +397,12 @@ Rect RenderedTarget::getBounds() const return Rect(left + m_x, top + m_y, right + m_x, bottom + m_y); } +QRectF scratchcpprender::RenderedTarget::getQmlBounds() const +{ + Rect bounds = getBounds(); + return QRectF(QPointF(bounds.left(), bounds.top()), QPointF(bounds.right(), bounds.bottom())); +} + QRectF RenderedTarget::getBoundsForBubble() const { // https://github.com/scratchfoundation/scratch-render/blob/86dcb0151a04bc8c1ff39559e8531e7921102b56/src/Drawable.js#L536-L551 @@ -401,20 +418,22 @@ QRectF RenderedTarget::getBoundsForBubble() const Rect RenderedTarget::getFastBounds() const { - if (!m_costume || !m_skin || !m_texture.isValid()) + if (!m_costume || !m_skin || !m_texture.isValid() || !m_cpuTexture.isValid()) return Rect(m_x, m_y, m_x, m_y); - const double scale = this->scale(); - const double width = this->width() / m_stageScale; - const double height = this->height() / m_stageScale; - const double originX = m_costume->rotationCenterX() * m_size / scale / m_costume->bitmapResolution() - width / 2; - const double originY = -m_costume->rotationCenterY() * m_size / scale / m_costume->bitmapResolution() + height / 2; + const double textureScale = m_skin->getTextureScale(m_cpuTexture); + const double width = m_cpuTexture.width() * m_size / textureScale; + const double height = m_cpuTexture.height() * m_size / textureScale; + const double originX = m_costume->rotationCenterX() * m_size / m_costume->bitmapResolution() - width / 2; + const double originY = -m_costume->rotationCenterY() * m_size / m_costume->bitmapResolution() + height / 2; const double rot = -rotation() * pi / 180; + const double sinRot = std::sin(rot); + const double cosRot = std::cos(rot); - QPointF topLeft = transformPoint(-width / 2, height / 2, originX, originY, rot); - QPointF topRight = transformPoint(width / 2, height / 2, originX, originY, rot); - QPointF bottomRight = transformPoint(width / 2, -height / 2, originX, originY, rot); - QPointF bottomLeft = transformPoint(-width / 2, -height / 2, originX, originY, rot); + QPointF topLeft = transformPoint(-width / 2, height / 2, originX, originY, sinRot, cosRot); + QPointF topRight = transformPoint(width / 2, height / 2, originX, originY, sinRot, cosRot); + QPointF bottomRight = transformPoint(width / 2, -height / 2, originX, originY, sinRot, cosRot); + QPointF bottomLeft = transformPoint(-width / 2, -height / 2, originX, originY, sinRot, cosRot); if (m_mirrorHorizontally) { topLeft.setX(-topLeft.x()); @@ -429,7 +448,7 @@ Rect RenderedTarget::getFastBounds() const const double minY = std::min(yList); const double maxY = std::max(yList); - return Rect(minX * scale + m_x, maxY * scale + m_y, maxX * scale + m_x, minY * scale + m_y); + return Rect(minX + m_x, maxY + m_y, maxX + m_x, minY + m_y); } QPointF RenderedTarget::mapFromScene(const QPointF &point) const @@ -516,6 +535,8 @@ void RenderedTarget::setGraphicEffect(ShaderManager::Effect effect, double value if (changed) update(); + + // TODO: Set m_convexHullDirty to true if the effect changes shape } void RenderedTarget::clearGraphicEffects() @@ -523,83 +544,108 @@ void RenderedTarget::clearGraphicEffects() if (!m_graphicEffects.empty()) update(); + // TODO: Set m_convexHullDirty to true if any of the previous effects changed shape m_graphicEffects.clear(); } -void RenderedTarget::updateHullPoints(QOpenGLFramebufferObject *fbo) +const std::vector &RenderedTarget::hullPoints() const { - Q_ASSERT(fbo); + if (convexHullPointsNeeded()) + const_cast(this)->updateHullPoints(); - if (!m_glF) { - m_glF = std::make_unique(); - m_glF->initializeOpenGLFunctions(); - } + return m_hullPoints; +} - int width = fbo->width(); - int height = fbo->height(); - m_hullPoints.clear(); - m_hullPoints.reserve(width * height); +bool RenderedTarget::contains(const QPointF &point) const +{ + if (!m_costume || !m_texture.isValid() || !m_cpuTexture.isValid() || !parentItem()) + return false; - // Read pixels from framebuffer - size_t size = width * height * 4; - GLubyte *pixelData = new GLubyte[size]; - m_glF->glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixelData); + const double scaleRatio = m_skin->getTextureScale(m_texture) / m_skin->getTextureScale(m_cpuTexture); + QPointF translatedPoint = mapToItem(parentItem(), point); + translatedPoint = mapFromStageWithOriginPoint(translatedPoint); + translatedPoint /= scaleRatio; - // Flip vertically - int rowSize = width * 4; - GLubyte *tempRow = new GLubyte[rowSize]; + if (!boundingRect().contains(translatedPoint)) + return false; - for (size_t i = 0; i < height / 2; ++i) { - size_t topRowIndex = i * rowSize; - size_t bottomRowIndex = (height - 1 - i) * rowSize; + const std::vector &points = hullPoints(); + QPoint intPoint = translatedPoint.toPoint(); + auto it = std::lower_bound(points.begin(), points.end(), intPoint, [](const QPointF &lhs, const QPointF &rhs) { return (lhs.y() < rhs.y()) || (lhs.y() == rhs.y() && lhs.x() < rhs.x()); }); - // Swap rows - memcpy(tempRow, &pixelData[topRowIndex], rowSize); - memcpy(&pixelData[topRowIndex], &pixelData[bottomRowIndex], rowSize); - memcpy(&pixelData[bottomRowIndex], tempRow, rowSize); + if (it == points.end()) { + // The point is beyond the last point in the convex hull + return false; } - delete[] tempRow; + // Check if the point is equal to the one found + return *it == intPoint; +} - // Fill hull points vector - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int index = (y * width + x) * 4; // RGBA channels +bool RenderedTarget::containsScratchPoint(double x, double y) const +{ + if (!m_engine || !parentItem()) + return false; - // Check alpha channel - if (pixelData[index + 3] > 0) - m_hullPoints.push_back(QPointF(x, y)); - } - } + // contains() expects item coordinates, so translate the Scratch coordinates first + double stageWidth = m_engine->stageWidth(); + double stageHeight = m_engine->stageHeight(); + x = m_stageScale * (x + stageWidth / 2); + y = m_stageScale * (stageHeight / 2 - y); - delete[] pixelData; + return contains(mapFromItem(parentItem(), QPointF(x, y))); } -const std::vector &RenderedTarget::hullPoints() const +bool RenderedTarget::touchingClones(const std::vector &clones) const { - return m_hullPoints; -} + // https://github.com/scratchfoundation/scratch-render/blob/941562438fe3dd6e7d98d9387607d535dcd68d24/src/RenderWebGL.js#L967-L1002 + // TODO: Use Rect methods and do not use QRects + Rect scratchRect = getFastBounds(); + const QRectF myRect(QPointF(scratchRect.left(), scratchRect.bottom()), QPointF(scratchRect.right(), scratchRect.top())); + QRectF united; + std::vector candidates; -bool RenderedTarget::contains(const QPointF &point) const -{ - if (m_stageModel) - return true; // the stage contains any point within the scene + // Calculate the union of the bounding rectangle intersections + for (auto clone : clones) { + Q_ASSERT(clone); - if (!boundingRect().contains(point)) - return false; + if (!clone) + continue; - QPoint intPoint = point.toPoint(); - auto it = std::lower_bound(m_hullPoints.begin(), m_hullPoints.end(), intPoint, [](const QPointF &lhs, const QPointF &rhs) { - return (lhs.y() < rhs.y()) || (lhs.y() == rhs.y() && lhs.x() < rhs.x()); - }); + SpriteModel *model = static_cast(clone->getInterface()); + Q_ASSERT(model); - if (it == m_hullPoints.end()) { - // The point is beyond the last point in the convex hull + if (model) { + // Calculate the intersection of the bounding rectangles + IRenderedTarget *candidate = model->renderedTarget(); + Q_ASSERT(candidate); + scratchRect = candidate->getFastBounds(); + QRectF rect(QPointF(scratchRect.left(), scratchRect.bottom()), QPointF(scratchRect.right(), scratchRect.top())); + QRectF intersected = myRect.intersected(rect); + + // Add it to the union + united = united.united(intersected); + + candidates.push_back(candidate); + } + } + + if (united.isEmpty() || candidates.empty()) return false; + + // Loop through the points of the union + for (int y = united.top(); y <= united.bottom(); y++) { + for (int x = united.left(); x <= united.right(); x++) { + if (this->containsScratchPoint(x, y)) { + for (IRenderedTarget *candidate : candidates) { + if (candidate->containsScratchPoint(x, y)) + return true; + } + } + } } - // Check if the point is equal to the one found - return *it == intPoint; + return false; } void RenderedTarget::calculatePos() @@ -659,10 +705,16 @@ void RenderedTarget::calculateRotation() void RenderedTarget::calculateSize() { if (m_skin && m_costume) { + GLuint oldTexture = m_cpuTexture.handle(); + bool wasValid = m_cpuTexture.isValid(); m_texture = m_skin->getTexture(m_size * m_stageScale); + m_cpuTexture = m_skin->getTexture(m_size); m_width = m_texture.width(); m_height = m_texture.height(); setScale(m_size * m_stageScale / m_skin->getTextureScale(m_texture) / m_costume->bitmapResolution()); + + if (wasValid && m_cpuTexture.handle() != oldTexture) + m_convexHullDirty = true; } } @@ -679,15 +731,61 @@ void RenderedTarget::handleSceneMouseMove(qreal x, qreal y) } } +bool RenderedTarget::convexHullPointsNeeded() const +{ + return m_convexHullDirty || m_hullPoints.empty(); +} + +void RenderedTarget::updateHullPoints() +{ + m_convexHullDirty = false; + + if (!isVisible()) { + m_hullPoints.clear(); + return; + } + + m_hullPoints = textureManager()->getTextureConvexHullPoints(m_cpuTexture); + // TODO: Apply graphic effects (#117) +} + QPointF RenderedTarget::transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const { - const double cosRot = std::cos(rot); - const double sinRot = std::sin(rot); + return transformPoint(scratchX, scratchY, originX, originY, std::sin(rot), std::cos(rot)); +} + +QPointF RenderedTarget::transformPoint(double scratchX, double scratchY, double originX, double originY, double sinRot, double cosRot) const +{ const double x = (scratchX - originX) * cosRot - (scratchY - originY) * sinRot; const double y = (scratchX - originX) * sinRot + (scratchY - originY) * cosRot; return QPointF(x, y); } +QPointF RenderedTarget::mapFromStageWithOriginPoint(const QPointF &scenePoint) const +{ + // mapFromItem() doesn't use the transformOriginPoint property, so we must do this ourselves + QTransform t; + const double mirror = m_mirrorHorizontally ? -1 : 1; + const double originX = transformOriginPoint().x(); + const double originY = transformOriginPoint().y(); + t.translate(originX, originY); + t.rotate(-rotation()); + t.scale(1 / scale() * mirror, 1 / scale()); + t.translate(-originX * mirror, -originY); + t.translate(-x(), -y()); + + QPointF localPoint = t.map(scenePoint); + return localPoint; +} + +CpuTextureManager *RenderedTarget::textureManager() +{ + if (!m_textureManager) + m_textureManager = std::make_shared(); + + return m_textureManager.get(); +} + bool RenderedTarget::mirrorHorizontally() const { return m_mirrorHorizontally; diff --git a/src/renderedtarget.h b/src/renderedtarget.h index d11f4d2..3b3d0c5 100644 --- a/src/renderedtarget.h +++ b/src/renderedtarget.h @@ -19,6 +19,7 @@ namespace scratchcpprender { class Skin; +class CpuTextureManager; class RenderedTarget : public IRenderedTarget { @@ -32,7 +33,7 @@ class RenderedTarget : public IRenderedTarget Q_PROPERTY(double stageScale READ stageScale WRITE setStageScale NOTIFY stageScaleChanged) public: - RenderedTarget(QNanoQuickItem *parent = nullptr); + RenderedTarget(QQuickItem *parent = nullptr); ~RenderedTarget(); void updateVisibility(bool visible) override; @@ -75,6 +76,7 @@ class RenderedTarget : public IRenderedTarget void setHeight(qreal height) override; libscratchcpp::Rect getBounds() const override; + Q_INVOKABLE QRectF getQmlBounds() const; Q_INVOKABLE QRectF getBoundsForBubble() const override; libscratchcpp::Rect getFastBounds() const override; @@ -88,10 +90,12 @@ class RenderedTarget : public IRenderedTarget void setGraphicEffect(ShaderManager::Effect effect, double value) override; void clearGraphicEffects() override; - void updateHullPoints(QOpenGLFramebufferObject *fbo) override; - const std::vector &hullPoints() const override; + const std::vector &hullPoints() const override; Q_INVOKABLE bool contains(const QPointF &point) const override; + bool containsScratchPoint(double x, double y) const override; + + bool touchingClones(const std::vector &) const override; signals: void engineChanged(); @@ -112,7 +116,12 @@ class RenderedTarget : public IRenderedTarget void calculateRotation(); void calculateSize(); void handleSceneMouseMove(qreal x, qreal y); + bool convexHullPointsNeeded() const; + void updateHullPoints(); QPointF transformPoint(double scratchX, double scratchY, double originX, double originY, double rot) const; + QPointF transformPoint(double scratchX, double scratchY, double originX, double originY, double sinRot, double cosRot) const; + QPointF mapFromStageWithOriginPoint(const QPointF &scenePoint) const; + CpuTextureManager *textureManager(); libscratchcpp::IEngine *m_engine = nullptr; libscratchcpp::Costume *m_costume = nullptr; @@ -125,6 +134,8 @@ class RenderedTarget : public IRenderedTarget Skin *m_skin = nullptr; Texture m_texture; Texture m_oldTexture; + Texture m_cpuTexture; // without stage scale + std::shared_ptr m_textureManager; // NOTE: Use textureManager()! std::unique_ptr m_glF; std::unordered_map m_graphicEffects; double m_size = 1; @@ -138,7 +149,8 @@ class RenderedTarget : public IRenderedTarget double m_stageScale = 1; qreal m_maximumWidth = std::numeric_limits::infinity(); qreal m_maximumHeight = std::numeric_limits::infinity(); - std::vector m_hullPoints; + bool m_convexHullDirty = true; + std::vector m_hullPoints; bool m_clicked = false; // left mouse button only! double m_dragDeltaX = 0; double m_dragDeltaY = 0; diff --git a/src/spritemodel.cpp b/src/spritemodel.cpp index 498838c..73eee92 100644 --- a/src/spritemodel.cpp +++ b/src/spritemodel.cpp @@ -150,6 +150,16 @@ libscratchcpp::Rect SpriteModel::fastBoundingRect() const return m_renderedTarget->getFastBounds(); } +bool SpriteModel::touchingClones(const std::vector &clones) const +{ + return m_renderedTarget->touchingClones(clones); +} + +bool SpriteModel::touchingPoint(double x, double y) const +{ + return m_renderedTarget->containsScratchPoint(x, y); +} + libscratchcpp::Sprite *SpriteModel::sprite() const { return m_sprite; diff --git a/src/spritemodel.h b/src/spritemodel.h index e6669c5..bc8da74 100644 --- a/src/spritemodel.h +++ b/src/spritemodel.h @@ -57,6 +57,9 @@ class SpriteModel libscratchcpp::Rect boundingRect() const override; libscratchcpp::Rect fastBoundingRect() const override; + bool touchingClones(const std::vector &clones) const override; + bool touchingPoint(double x, double y) const override; + libscratchcpp::Sprite *sprite() const; IRenderedTarget *renderedTarget() const; diff --git a/src/stagemodel.cpp b/src/stagemodel.cpp index 1379c80..d57a0af 100644 --- a/src/stagemodel.cpp +++ b/src/stagemodel.cpp @@ -77,6 +77,26 @@ void StageModel::onBubbleTextChanged(const std::string &text) } } +libscratchcpp::Rect StageModel::boundingRect() const +{ + return libscratchcpp::Rect(); +} + +libscratchcpp::Rect StageModel::fastBoundingRect() const +{ + return libscratchcpp::Rect(); +} + +bool StageModel::touchingClones(const std::vector &clones) const +{ + return m_renderedTarget->touchingClones(clones); +} + +bool StageModel::touchingPoint(double x, double y) const +{ + return m_renderedTarget->containsScratchPoint(x, y); +} + void StageModel::loadCostume() { if (m_renderedTarget && m_stage) diff --git a/src/stagemodel.h b/src/stagemodel.h index 11e3929..e70ef81 100644 --- a/src/stagemodel.h +++ b/src/stagemodel.h @@ -40,6 +40,12 @@ class StageModel void onBubbleTypeChanged(libscratchcpp::Target::BubbleType type) override; void onBubbleTextChanged(const std::string &text) override; + libscratchcpp::Rect boundingRect() const override; + libscratchcpp::Rect fastBoundingRect() const override; + + bool touchingClones(const std::vector &clones) const override; + bool touchingPoint(double x, double y) const override; + Q_INVOKABLE void loadCostume(); libscratchcpp::Stage *stage() const; diff --git a/src/targetpainter.cpp b/src/targetpainter.cpp index d25b572..bd1cc40 100644 --- a/src/targetpainter.cpp +++ b/src/targetpainter.cpp @@ -99,7 +99,7 @@ void TargetPainter::paint(QNanoPainter *painter) // Process the resulting texture // NOTE: This must happen now, not later, because the alpha channel can be used here - m_target->updateHullPoints(targetFbo); + // Currently nothing is happening here... // Cleanup shaderProgram->release(); diff --git a/test/mocks/enginemock.h b/test/mocks/enginemock.h index bae97e2..89db942 100644 --- a/test/mocks/enginemock.h +++ b/test/mocks/enginemock.h @@ -34,7 +34,8 @@ class EngineMock : public IEngine MOCK_METHOD(void, runEventLoop, (), (override)); MOCK_METHOD(void, stopEventLoop, (), (override)); - MOCK_METHOD(void, setRedrawHandler, (const std::function &), (override)); + MOCK_METHOD(sigslot::signal<> &, aboutToRender, (), (override)); + MOCK_METHOD(sigslot::signal &, threadAboutToStop, (), (override)); MOCK_METHOD(bool, isRunning, (), (const, override)); @@ -86,8 +87,10 @@ class EngineMock : public IEngine MOCK_METHOD(void, registerSection, (std::shared_ptr), (override)); MOCK_METHOD(unsigned int, functionIndex, (BlockFunc), (override)); + MOCK_METHOD(const std::vector &, blockFunctions, (), (const, override)); MOCK_METHOD(void, addCompileFunction, (IBlockSection *, const std::string &, BlockComp), (override)); + MOCK_METHOD(void, addHatPredicateCompileFunction, (IBlockSection *, const std::string &, HatPredicateCompileFunc), (override)); MOCK_METHOD(void, addMonitorNameFunction, (IBlockSection *, const std::string &, MonitorNameFunc), (override)); MOCK_METHOD(void, addMonitorChangeFunction, (IBlockSection *, const std::string &, MonitorChangeFunc), (override)); MOCK_METHOD(void, addHatBlock, (IBlockSection *, const std::string &), (override)); @@ -107,6 +110,7 @@ class EngineMock : public IEngine MOCK_METHOD(void, addCloneInitScript, (std::shared_ptr), (override)); MOCK_METHOD(void, addKeyPressScript, (std::shared_ptr, int), (override)); MOCK_METHOD(void, addTargetClickScript, (std::shared_ptr), (override)); + MOCK_METHOD(void, addWhenGreaterThanScript, (std::shared_ptr), (override)); MOCK_METHOD(const std::vector> &, targets, (), (const, override)); MOCK_METHOD(void, setTargets, (const std::vector> &), (override)); @@ -123,14 +127,11 @@ class EngineMock : public IEngine MOCK_METHOD(const std::vector> &, monitors, (), (const, override)); MOCK_METHOD(void, setMonitors, (const std::vector> &), (override)); - MOCK_METHOD(void, setAddMonitorHandler, (const std::function &), (override)); - MOCK_METHOD(void, setRemoveMonitorHandler, (const std::function &), (override)); + MOCK_METHOD(sigslot::signal &, monitorAdded, (), (override)); + MOCK_METHOD((sigslot::signal &), monitorRemoved, (), (override)); - MOCK_METHOD(const std::function &, questionAsked, (), (const, override)); - MOCK_METHOD(void, setQuestionAsked, (const std::function &), (override)); - - MOCK_METHOD(const std::function &, questionAnswered, (), (const, override)); - MOCK_METHOD(void, setQuestionAnswered, (const std::function &), (override)); + MOCK_METHOD(sigslot::signal &, questionAsked, (), (override)); + MOCK_METHOD(sigslot::signal &, questionAnswered, (), (override)); MOCK_METHOD(std::vector &, extensions, (), (const, override)); MOCK_METHOD(void, setExtensions, (const std::vector &), (override)); diff --git a/test/mocks/renderedtargetmock.h b/test/mocks/renderedtargetmock.h index 695253e..ee2974e 100644 --- a/test/mocks/renderedtargetmock.h +++ b/test/mocks/renderedtargetmock.h @@ -67,10 +67,13 @@ class RenderedTargetMock : public IRenderedTarget MOCK_METHOD(void, setGraphicEffect, (ShaderManager::Effect effect, double value), (override)); MOCK_METHOD(void, clearGraphicEffects, (), (override)); - MOCK_METHOD(void, updateHullPoints, (QOpenGLFramebufferObject *), (override)); - MOCK_METHOD(const std::vector &, hullPoints, (), (const, override)); + MOCK_METHOD(const std::vector &, hullPoints, (), (const, override)); MOCK_METHOD(bool, contains, (const QPointF &), (const, override)); + MOCK_METHOD(bool, containsScratchPoint, (double, double), (const, override)); + + MOCK_METHOD(bool, touchingClones, (const std::vector &), (const, override)); + MOCK_METHOD(QNanoQuickItemPainter *, createItemPainter, (), (const, override)); MOCK_METHOD(void, hoverEnterEvent, (QHoverEvent *), (override)); MOCK_METHOD(void, hoverLeaveEvent, (QHoverEvent *), (override)); diff --git a/test/projectloader/projectloader_test.cpp b/test/projectloader/projectloader_test.cpp index 64e83da..29ba171 100644 --- a/test/projectloader/projectloader_test.cpp +++ b/test/projectloader/projectloader_test.cpp @@ -96,7 +96,7 @@ TEST_F(ProjectLoaderTest, Load) ASSERT_EQ(sprites[1]->sprite(), engine->targetAt(2)); const auto &monitors = loader.monitorList(); - ASSERT_EQ(monitors.size(), 10); + ASSERT_EQ(monitors.size(), 11); ListMonitorModel *listMonitorModel = dynamic_cast(monitors[0]); ASSERT_EQ(listMonitorModel->monitor(), engine->monitors().at(0).get()); @@ -179,10 +179,8 @@ TEST_F(ProjectLoaderTest, QuestionAsked) load(&loader, "load_test.sb3"); auto engine = loader.engine(); - auto f = engine->questionAsked(); - ASSERT_TRUE(f); ASSERT_TRUE(spy.isEmpty()); - f("test"); + engine->questionAsked()("test"); ASSERT_EQ(spy.count(), 1); auto args = spy.takeFirst(); @@ -197,8 +195,9 @@ TEST_F(ProjectLoaderTest, AnswerQuestion) loader.setEngine(&engine); AnswerQuestionMock mock; - std::function f = std::bind(&AnswerQuestionMock::answer, &mock, std::placeholders::_1); - EXPECT_CALL(engine, questionAnswered()).WillOnce(ReturnRef(f)); + sigslot::signal answered; + answered.connect(&AnswerQuestionMock::answer, &mock); + EXPECT_CALL(engine, questionAnswered()).WillOnce(ReturnRef(answered)); EXPECT_CALL(mock, answer("hello")); loader.answerQuestion("hello"); } diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index fb02831..a24edab 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "../common.h" @@ -43,10 +44,10 @@ class RenderedTargetTest : public testing::Test TEST_F(RenderedTargetTest, Constructors) { - RenderedTarget target1; - RenderedTarget target2(&target1); - ASSERT_EQ(target2.parent(), &target1); - ASSERT_EQ(target2.parentItem(), &target1); + QQuickItem item1; + QQuickItem item2(&item1); + ASSERT_EQ(item2.parent(), &item1); + ASSERT_EQ(item2.parentItem(), &item1); } TEST_F(RenderedTargetTest, UpdateMethods) @@ -305,7 +306,10 @@ TEST_F(RenderedTargetTest, HullPoints) SpriteModel model; model.init(&sprite); - RenderedTarget target; + QQuickItem parent; + parent.setWidth(480); + parent.setHeight(360); + RenderedTarget target(&parent); target.setEngine(&engine); target.setSpriteModel(&model); @@ -314,27 +318,23 @@ TEST_F(RenderedTargetTest, HullPoints) QOffscreenSurface surface; createContextAndSurface(&context, &surface); - // Create a painter - QNanoPainter painter; - - QOpenGLFramebufferObjectFormat format; - format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); - - // Begin painting - QOpenGLFramebufferObject fbo(4, 6, format); - fbo.bind(); - painter.beginFrame(fbo.width(), fbo.height()); - - // Paint - QNanoImage image = QNanoImage::fromCache(&painter, "image.png"); - painter.drawImage(image, 0, 0); - painter.endFrame(); + // Load costume + EXPECT_CALL(engine, stageWidth()).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillRepeatedly(Return(360)); + auto costume = std::make_shared("", "", "png"); + std::string costumeData = readFileStr("image.png"); + costume->setData(costumeData.size(), static_cast(costumeData.data())); + sprite.addCostume(costume); + target.loadCostumes(); + target.updateCostume(costume.get()); + target.setStageScale(2); + target.setX(25); + target.setY(30); // Test hull points target.setWidth(3); target.setHeight(3); - 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 } })); + ASSERT_EQ(target.hullPoints(), std::vector({ { 1, 1 }, { 2, 1 }, { 3, 1 }, { 1, 2 }, { 3, 2 }, { 1, 3 }, { 2, 3 }, { 3, 3 } })); // Test contains() ASSERT_FALSE(target.contains({ 0, 0 })); @@ -358,49 +358,45 @@ TEST_F(RenderedTargetTest, HullPoints) ASSERT_TRUE(target.contains({ 3, 3 })); ASSERT_FALSE(target.contains({ 3.3, 3.5 })); - // Stage: hull points - Stage stage; - StageModel stageModel; - stageModel.init(&stage); - target.setSpriteModel(nullptr); - target.setStageModel(&stageModel); - - target.setWidth(3); - target.setHeight(3); - fbo.release(); - QOpenGLFramebufferObject emptyFbo(fbo.size(), format); - emptyFbo.bind(); - target.updateHullPoints(&emptyFbo); // clear the convex hull points list - ASSERT_TRUE(target.hullPoints().empty()); - emptyFbo.release(); - fbo.bind(); - 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 } })); - - // Stage: contains() - ASSERT_TRUE(target.contains({ 0, 0 })); - ASSERT_TRUE(target.contains({ 1, 0 })); - ASSERT_TRUE(target.contains({ 2, 0 })); - ASSERT_TRUE(target.contains({ 3, 0 })); - - ASSERT_TRUE(target.contains({ 0, 1 })); - ASSERT_TRUE(target.contains({ 1, 1 })); - ASSERT_TRUE(target.contains({ 1.4, 1.25 })); - ASSERT_TRUE(target.contains({ 2, 1 })); - ASSERT_TRUE(target.contains({ 3, 1 })); - - ASSERT_TRUE(target.contains({ 1, 2 })); - ASSERT_TRUE(target.contains({ 2, 2 })); - ASSERT_TRUE(target.contains({ 3, 2 })); - ASSERT_TRUE(target.contains({ 3.5, 2.1 })); - - ASSERT_TRUE(target.contains({ 1, 3 })); - ASSERT_TRUE(target.contains({ 2, 3 })); - ASSERT_TRUE(target.contains({ 3, 3 })); - ASSERT_TRUE(target.contains({ 3.3, 3.5 })); - - // Release - fbo.release(); + // Test contains() with horizontal mirroring + target.updateRotationStyle(Sprite::RotationStyle::LeftRight); + target.updateDirection(-45); + target.setX(25); + target.setY(30); + ASSERT_FALSE(target.contains({ 0, 0 })); + ASSERT_TRUE(target.contains({ -1, 1 })); + ASSERT_FALSE(target.contains({ -2, 2 })); + ASSERT_TRUE(target.contains({ -3, 2 })); + ASSERT_FALSE(target.contains({ -3.5, 2.1 })); + ASSERT_TRUE(target.contains({ -2, 3 })); + ASSERT_FALSE(target.contains({ -3.3, 3.5 })); + + // Test containsScratchPoint() + target.updateDirection(0); + target.setX(25); + target.setY(30); + ASSERT_FALSE(target.containsScratchPoint(-227.5, 165)); // [0, 0] + ASSERT_FALSE(target.containsScratchPoint(-226.5, 165)); // [1, 0] + ASSERT_FALSE(target.containsScratchPoint(-225.5, 165)); // [2, 0] + ASSERT_FALSE(target.containsScratchPoint(-224.5, 165)); // [3, 0] + + ASSERT_FALSE(target.containsScratchPoint(-227.5, 164)); // [0, 1] + ASSERT_TRUE(target.containsScratchPoint(-226.5, 164)); // [1, 1] + ASSERT_TRUE(target.containsScratchPoint(-226.1, 163.75)); // [1.4, 1.25] + ASSERT_TRUE(target.containsScratchPoint(-225.5, 164)); // [2, 1] + ASSERT_TRUE(target.containsScratchPoint(-224.5, 164)); // [3, 1] + + ASSERT_TRUE(target.containsScratchPoint(-226.5, 163)); // [1, 2] + ASSERT_FALSE(target.containsScratchPoint(-225.5, 163)); // [2, 2] + ASSERT_TRUE(target.containsScratchPoint(-224.5, 163)); // [3, 2] + ASSERT_FALSE(target.containsScratchPoint(-224, 162.9)); // [3.5, 2.1] + + ASSERT_TRUE(target.containsScratchPoint(-226.5, 162)); // [1, 3] + ASSERT_TRUE(target.containsScratchPoint(-225.5, 162)); // [2, 3] + ASSERT_TRUE(target.containsScratchPoint(-224.5, 162)); // [3, 3] + ASSERT_FALSE(target.containsScratchPoint(-224.2, 161.5)); // [3.3, 3.5] + + // Cleanup context.doneCurrent(); } @@ -640,8 +636,6 @@ TEST_F(RenderedTargetTest, GetBounds) QOpenGLContext context; QOffscreenSurface surface; createContextAndSurface(&context, &surface); - QOpenGLExtraFunctions glF(&context); - glF.initializeOpenGLFunctions(); RenderedTarget target; Sprite sprite; @@ -668,75 +662,65 @@ TEST_F(RenderedTargetTest, GetBounds) 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); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 64.96); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, -121.16); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, 67.79); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -123.99); QRectF bubbleBounds = target.getBoundsForBubble(); - ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, 66.13); - ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, -124.52); - ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, 66.72); - ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, -125.11); + ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, 64.96); + ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, -121.16); + ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, 67.79); + ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, -123.99); 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); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 69.5); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, -111.26); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, 71.5); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -113.26); bubbleBounds = target.getBoundsForBubble(); - ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, 71.87); - ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, -110.47); - ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, 72.29); - ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, -110.89); + ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, 69.5); + ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, -111.26); + ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, 71.5); + ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, -113.26); 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); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 69.5); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, -111.26); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, 71.5); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -113.26); bubbleBounds = target.getBoundsForBubble(); - ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, 71.87); - ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, -110.47); - ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, 72.29); - ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, -110.89); + ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, 69.5); + ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, -111.26); + ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, 71.5); + ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, -113.26); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.updateSize(9780.6); bounds = target.getBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, -466.05); - ASSERT_EQ(std::round(bounds.top() * 100) / 100, 1294.13); - ASSERT_EQ(std::round(bounds.right() * 100) / 100, -405.87); - ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, 1233.94); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, -378.77); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, 1323.22); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, -376.77); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, 1321.22); bubbleBounds = target.getBoundsForBubble(); - ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, -466.05); - ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, 1294.13); - ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, -405.87); - ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, 1286.13); + ASSERT_EQ(std::round(bubbleBounds.left() * 100) / 100, -378.77); + ASSERT_EQ(std::round(bubbleBounds.top() * 100) / 100, 1323.22); + ASSERT_EQ(std::round(bubbleBounds.right() * 100) / 100, -376.77); + ASSERT_EQ(std::round(bubbleBounds.bottom() * 100) / 100, 1321.22); context.doneCurrent(); } @@ -775,9 +759,9 @@ TEST_F(RenderedTargetTest, GetFastBounds) target.beforeRedraw(); Rect bounds = target.getFastBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, 65.84); - ASSERT_EQ(std::round(bounds.top() * 100) / 100, -123.92); - ASSERT_EQ(std::round(bounds.right() * 100) / 100, 67.31); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 64.47); + ASSERT_EQ(std::round(bounds.top() * 100) / 100, -120.57); + ASSERT_EQ(std::round(bounds.right() * 100) / 100, 69.26); ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -125.4); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); @@ -785,30 +769,135 @@ TEST_F(RenderedTargetTest, GetFastBounds) target.updateRotationStyle(Sprite::RotationStyle::LeftRight); bounds = target.getFastBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, 71.67); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 69.78); ASSERT_EQ(std::round(bounds.top() * 100) / 100, -110.26); ASSERT_EQ(std::round(bounds.right() * 100) / 100, 72.5); - ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -111.51); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -114.34); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.setStageScale(20.75); bounds = target.getFastBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, 71.67); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, 69.78); ASSERT_EQ(std::round(bounds.top() * 100) / 100, -110.26); ASSERT_EQ(std::round(bounds.right() * 100) / 100, 72.5); - ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -111.51); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, -114.34); EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); target.updateSize(9780.6); bounds = target.getFastBounds(); - ASSERT_EQ(std::round(bounds.left() * 100) / 100, -496.15); + ASSERT_EQ(std::round(bounds.left() * 100) / 100, -767); ASSERT_EQ(std::round(bounds.top() * 100) / 100, 1324.22); ASSERT_EQ(std::round(bounds.right() * 100) / 100, -375.77); - ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, 1143.65); + ASSERT_EQ(std::round(bounds.bottom() * 100) / 100, 737.38); + + context.doneCurrent(); +} + +TEST_F(RenderedTargetTest, TouchingClones) +{ + EngineMock engine; + Sprite sprite, clone1, clone2; + SpriteModel model, model1, model2; + model.init(&sprite); + clone1.setInterface(&model1); + clone2.setInterface(&model2); + + QQuickItem parent; + parent.setWidth(480); + parent.setHeight(360); + + RenderedTarget target(&parent); + target.setEngine(&engine); + target.setSpriteModel(&model); + + RenderedTargetMock target1, target2; + model1.setRenderedTarget(&target1); + model2.setRenderedTarget(&target2); + + // Create OpenGL context + QOpenGLContext context; + QOffscreenSurface surface; + createContextAndSurface(&context, &surface); + + // Load costume + EXPECT_CALL(engine, stageWidth()).WillRepeatedly(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillRepeatedly(Return(360)); + auto costume = std::make_shared("", "", "png"); + std::string costumeData = readFileStr("image.png"); + costume->setData(costumeData.size(), static_cast(costumeData.data())); + sprite.addCostume(costume); + target.loadCostumes(); + target.updateCostume(costume.get()); + target.setWidth(3); + target.setHeight(3); + EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2, 1, 6, -5))); + EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -1, 1.8, -8))); + EXPECT_CALL(target1, containsScratchPoint(1, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(2, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(2, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(3, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(3, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(1, -2)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -2)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(3, -2)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(3, -2)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(1, -1)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -1)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(2, -1)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(2, -1)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(3, -1)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(3, -1)).WillOnce(Return(false)); + ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 })); + + EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2, 1, 6, -5))); + EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -1, 1.8, -8))); + EXPECT_CALL(target1, containsScratchPoint(1, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(2, -3)).WillOnce(Return(true)); + ASSERT_TRUE(target.touchingClones({ &clone1, &clone2 })); + + EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(5, 1, 6, -5))); + EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -1, 1.8, -8))); + EXPECT_CALL(target1, containsScratchPoint(1, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(1, -2)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -2)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(1, -1)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(1, -1)).WillOnce(Return(false)); + ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 })); + + EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2, 1, 6, -5))); + EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -6.5, 1.8, -8))); + EXPECT_CALL(target1, containsScratchPoint(2, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(2, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(3, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(3, -3)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(3, -2)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(3, -2)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(2, -1)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(2, -1)).WillOnce(Return(false)); + EXPECT_CALL(target1, containsScratchPoint(3, -1)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(3, -1)).WillOnce(Return(false)); + ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 })); + + EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(2, 1, 6, -5))); + EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -6.5, 1.8, -8))); + EXPECT_CALL(target1, containsScratchPoint(2, -3)).WillOnce(Return(false)); + EXPECT_CALL(target2, containsScratchPoint(2, -3)).WillOnce(Return(true)); + ASSERT_TRUE(target.touchingClones({ &clone1, &clone2 })); + + EXPECT_CALL(target1, getFastBounds()).WillOnce(Return(Rect(5, 1, 6, -5))); + EXPECT_CALL(target2, getFastBounds()).WillOnce(Return(Rect(-5, -6.5, 1.8, -8))); + EXPECT_CALL(target1, containsScratchPoint).Times(0); + EXPECT_CALL(target2, containsScratchPoint).Times(0); + ASSERT_FALSE(target.touchingClones({ &clone1, &clone2 })); + + // Cleanup context.doneCurrent(); } diff --git a/test/target_models/spritemodel_test.cpp b/test/target_models/spritemodel_test.cpp index 7d3409b..6f44f88 100644 --- a/test/target_models/spritemodel_test.cpp +++ b/test/target_models/spritemodel_test.cpp @@ -318,6 +318,46 @@ TEST(SpriteModelTest, FastBoundingRect) ASSERT_EQ(bounds.bottom(), rect.bottom()); } +TEST(SpriteModelTest, TouchingClones) +{ + SpriteModel model; + + RenderedTargetMock renderedTarget; + model.setRenderedTarget(&renderedTarget); + + Sprite clone1, clone2; + std::vector clones = { &clone1, &clone2 }; + std::vector actualClones; + + EXPECT_CALL(renderedTarget, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&actualClones](const std::vector &candidates) { + actualClones = candidates; + return false; + }))); + ASSERT_FALSE(model.touchingClones(clones)); + ASSERT_EQ(actualClones, clones); + + EXPECT_CALL(renderedTarget, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&actualClones](const std::vector &candidates) { + actualClones = candidates; + return true; + }))); + ASSERT_TRUE(model.touchingClones(clones)); + ASSERT_EQ(actualClones, clones); +} + +TEST(SpriteModelTest, TouchingPoint) +{ + SpriteModel model; + + RenderedTargetMock renderedTarget; + model.setRenderedTarget(&renderedTarget); + + EXPECT_CALL(renderedTarget, containsScratchPoint(56.3, -179.4)).WillOnce(Return(false)); + ASSERT_FALSE(model.touchingPoint(56.3, -179.4)); + + EXPECT_CALL(renderedTarget, containsScratchPoint(-20.08, 109.47)).WillOnce(Return(true)); + ASSERT_TRUE(model.touchingPoint(-20.08, 109.47)); +} + TEST(SpriteModelTest, RenderedTarget) { SpriteModel model; diff --git a/test/target_models/stagemodel_test.cpp b/test/target_models/stagemodel_test.cpp index 66a50cc..2c4bf69 100644 --- a/test/target_models/stagemodel_test.cpp +++ b/test/target_models/stagemodel_test.cpp @@ -9,6 +9,11 @@ using namespace scratchcpprender; using namespace libscratchcpp; +using ::testing::Return; +using ::testing::WithArgs; +using ::testing::Invoke; +using ::testing::_; + TEST(StageModelTest, Constructors) { StageModel model1; @@ -106,6 +111,46 @@ TEST(StageModelTest, OnBubbleTextChanged) ASSERT_EQ(spy.count(), 2); } +TEST(SpriteModelTest, TouchingClones) +{ + StageModel model; + + RenderedTargetMock renderedTarget; + model.setRenderedTarget(&renderedTarget); + + Sprite clone1, clone2; + std::vector clones = { &clone1, &clone2 }; + std::vector actualClones; + + EXPECT_CALL(renderedTarget, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&actualClones](const std::vector &candidates) { + actualClones = candidates; + return false; + }))); + ASSERT_FALSE(model.touchingClones(clones)); + ASSERT_EQ(actualClones, clones); + + EXPECT_CALL(renderedTarget, touchingClones(_)).WillOnce(WithArgs<0>(Invoke([&actualClones](const std::vector &candidates) { + actualClones = candidates; + return true; + }))); + ASSERT_TRUE(model.touchingClones(clones)); + ASSERT_EQ(actualClones, clones); +} + +TEST(StageModelTest, TouchingPoint) +{ + StageModel model; + + RenderedTargetMock renderedTarget; + model.setRenderedTarget(&renderedTarget); + + EXPECT_CALL(renderedTarget, containsScratchPoint(56.3, -179.4)).WillOnce(Return(false)); + ASSERT_FALSE(model.touchingPoint(56.3, -179.4)); + + EXPECT_CALL(renderedTarget, containsScratchPoint(-20.08, 109.47)).WillOnce(Return(true)); + ASSERT_TRUE(model.touchingPoint(-20.08, 109.47)); +} + TEST(StageModelTest, RenderedTarget) { StageModel model; diff --git a/test/targetpainter/targetpainter_test.cpp b/test/targetpainter/targetpainter_test.cpp index 1d4c7b3..57175be 100644 --- a/test/targetpainter/targetpainter_test.cpp +++ b/test/targetpainter/targetpainter_test.cpp @@ -82,7 +82,6 @@ TEST_F(TargetPainterTest, Paint) std::unordered_map effects; EXPECT_CALL(target, texture()).WillOnce(Return(texture)); EXPECT_CALL(target, graphicEffects()).WillOnce(ReturnRef(effects)); - EXPECT_CALL(target, updateHullPoints(&fbo)); targetPainter.paint(&painter); painter.endFrame(); @@ -97,7 +96,6 @@ TEST_F(TargetPainterTest, Paint) effects[ShaderManager::Effect::Ghost] = 84; EXPECT_CALL(target, texture()).WillOnce(Return(texture)); EXPECT_CALL(target, graphicEffects()).WillOnce(ReturnRef(effects)); - EXPECT_CALL(target, updateHullPoints(&fbo)); targetPainter.paint(&painter); painter.endFrame(); effects.clear(); diff --git a/test/texture/CMakeLists.txt b/test/texture/CMakeLists.txt index 18d74a5..c4c2f04 100644 --- a/test/texture/CMakeLists.txt +++ b/test/texture/CMakeLists.txt @@ -1,3 +1,4 @@ +# texture_test add_executable( texture_test texture_test.cpp @@ -12,3 +13,19 @@ target_link_libraries( add_test(texture_test) gtest_discover_tests(texture_test) + +# cputexturemanager_test +add_executable( + cputexturemanager_test + cputexturemanager_test.cpp +) + +target_link_libraries( + cputexturemanager_test + GTest::gtest_main + scratchcpp-render + ${QT_LIBS} +) + +add_test(cputexturemanager_test) +gtest_discover_tests(cputexturemanager_test) diff --git a/test/texture/cputexturemanager_test.cpp b/test/texture/cputexturemanager_test.cpp new file mode 100644 index 0000000..375f59c --- /dev/null +++ b/test/texture/cputexturemanager_test.cpp @@ -0,0 +1,107 @@ +#include +#include +#include + +#include "../common.h" + +using namespace scratchcpprender; + +class CpuTextureManagerTest : public testing::Test +{ + public: + void createContextAndSurface(QOpenGLContext *context, QOffscreenSurface *surface) + { + QSurfaceFormat surfaceFormat; + surfaceFormat.setMajorVersion(4); + surfaceFormat.setMinorVersion(3); + + context->setFormat(surfaceFormat); + context->create(); + ASSERT_TRUE(context->isValid()); + + surface->setFormat(surfaceFormat); + surface->create(); + ASSERT_TRUE(surface->isValid()); + + context->makeCurrent(surface); + ASSERT_EQ(QOpenGLContext::currentContext(), context); + } +}; + +class ImagePainter +{ + public: + ImagePainter(QNanoPainter *painter, const QString &fileName) + { + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + + // Begin painting + m_fbo = std::make_unique(4, 6, format); + m_fbo->bind(); + painter->beginFrame(m_fbo->width(), m_fbo->height()); + + // Paint + QNanoImage image = QNanoImage::fromCache(painter, fileName); + painter->drawImage(image, 0, 0); + painter->endFrame(); + } + + ~ImagePainter() { m_fbo->release(); } + + QOpenGLFramebufferObject *fbo() const { return m_fbo.get(); }; + + private: + std::unique_ptr m_fbo; +}; + +TEST_F(CpuTextureManagerTest, TextureDataAndHullPoints) +{ + static const GLubyte refData1[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 0, 255, 255, 255, 128, 128, 255, 0, 0, 0, 0, 0, 0, 128, 255, 0, 0, 0, 0, 87, 149, 87, 149, + 0, 0, 0, 0, 128, 0, 128, 255, 128, 128, 255, 255, 128, 128, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + + static const GLubyte refData2[] = { + 0, 0, 57, 255, 10, 0, 50, 255, 43, 0, 35, 255, 60, 0, 28, 255, 0, 0, 55, 255, 39, 15, 73, 255, 137, 85, 133, 255, 207, 142, 182, 255, + 10, 0, 50, 255, 23, 4, 50, 255, 4, 0, 7, 255, 204, 204, 196, 255, 11, 0, 35, 255, 59, 46, 76, 255, 135, 146, 140, 255, 99, 123, 99, 255, + 4, 0, 12, 255, 1, 0, 7, 255, 0, 1, 0, 255, 0, 3, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255 + }; + + static const std::vector refHullPoints1 = { { 1, 1 }, { 2, 1 }, { 3, 1 }, { 1, 2 }, { 3, 2 }, { 1, 3 }, { 2, 3 }, { 3, 3 } }; + + static const std::vector refHullPoints2 = { + { 0, 0 }, { 1, 0 }, { 2, 0 }, { 3, 0 }, { 0, 1 }, { 1, 1 }, { 2, 1 }, { 3, 1 }, { 0, 2 }, { 1, 2 }, { 2, 2 }, { 3, 2 }, + { 0, 3 }, { 1, 3 }, { 2, 3 }, { 3, 3 }, { 0, 4 }, { 1, 4 }, { 2, 4 }, { 3, 4 }, { 0, 5 }, { 1, 5 }, { 2, 5 }, { 3, 5 } + }; + + // Create OpenGL context + QOpenGLContext context; + QOffscreenSurface surface; + createContextAndSurface(&context, &surface); + + // Paint images + QNanoPainter painter; + ImagePainter imgPainter1(&painter, "image.png"); + ImagePainter imgPainter2(&painter, "image.jpg"); + + // Read texture data + CpuTextureManager manager; + + for (int i = 0; i < 2; i++) { + Texture texture1(imgPainter1.fbo()->texture(), imgPainter1.fbo()->size()); + GLubyte *data = manager.getTextureData(texture1); + ASSERT_EQ(memcmp(data, refData1, 96), 0); + const auto &hullPoints1 = manager.getTextureConvexHullPoints(texture1); + ASSERT_EQ(hullPoints1, refHullPoints1); + + Texture texture2(imgPainter2.fbo()->texture(), imgPainter2.fbo()->size()); + data = manager.getTextureData(texture2); + ASSERT_EQ(memcmp(data, refData2, 96), 0); + const auto &hullPoints2 = manager.getTextureConvexHullPoints(texture2); + ASSERT_EQ(hullPoints2, refHullPoints2); + } + + // Cleanup + context.doneCurrent(); +}