diff --git a/ScratchCPPGui/renderedtarget.cpp b/ScratchCPPGui/renderedtarget.cpp index 308b807..d3f0185 100644 --- a/ScratchCPPGui/renderedtarget.cpp +++ b/ScratchCPPGui/renderedtarget.cpp @@ -13,9 +13,30 @@ using namespace scratchcppgui; using namespace libscratchcpp; +static const double SVG_SCALE_LIMIT = 0.1; // the maximum viewport dimensions are multiplied by this + 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(); + } + } } void RenderedTarget::loadProperties() @@ -31,6 +52,9 @@ void RenderedTarget::loadProperties() // Visibility m_visible = sprite->visible(); + m_size = sprite->size() / 100; + updateCostumeData(); + if (m_visible) { // Direction switch (sprite->rotationStyle()) { @@ -54,11 +78,11 @@ void RenderedTarget::loadProperties() } // Coordinates - double size = sprite->size() / 100; - m_x = static_cast(m_engine->stageWidth()) / 2 + sprite->x() - m_costume->rotationCenterX() * size / m_costume->bitmapResolution() * (m_newMirrorHorizontally ? -1 : 1); - m_y = static_cast(m_engine->stageHeight()) / 2 - sprite->y() - m_costume->rotationCenterY() * size / m_costume->bitmapResolution(); - m_originX = m_costume->rotationCenterX() * size / m_costume->bitmapResolution(); - m_originY = m_costume->rotationCenterY() * size / m_costume->bitmapResolution(); + double clampedSize = std::min(m_size, m_maxSize); + m_x = static_cast(m_engine->stageWidth()) / 2 + sprite->x() - m_costume->rotationCenterX() * clampedSize / m_costume->bitmapResolution() * (m_newMirrorHorizontally ? -1 : 1); + m_y = static_cast(m_engine->stageHeight()) / 2 - sprite->y() - m_costume->rotationCenterY() * clampedSize / m_costume->bitmapResolution(); + m_originX = m_costume->rotationCenterX() * clampedSize / m_costume->bitmapResolution(); + m_originY = m_costume->rotationCenterY() * clampedSize / m_costume->bitmapResolution(); // Layer m_z = sprite->layerOrder(); @@ -66,6 +90,7 @@ void RenderedTarget::loadProperties() mutex.unlock(); } else if (m_stageModel) { + updateCostumeData(); m_x = static_cast(m_engine->stageWidth()) / 2 - m_costume->rotationCenterX() / m_costume->bitmapResolution(); m_y = static_cast(m_engine->stageHeight()) / 2 - m_costume->rotationCenterY() / m_costume->bitmapResolution(); m_originX = m_costume->rotationCenterX() / m_costume->bitmapResolution(); @@ -79,13 +104,8 @@ void RenderedTarget::loadCostume(Costume *costume) return; m_costumeMutex.lock(); - m_imageChanged = true; - - if (costume->dataFormat() == "svg") { - if (costume != m_costume) - m_svgRenderer.load(QByteArray::fromRawData(static_cast(costume->data()), costume->dataSize())); - } - + m_loadCostume = true; + m_costumeChanged = (costume != m_costume); m_costume = costume; m_costumeMutex.unlock(); } @@ -97,7 +117,6 @@ void RenderedTarget::updateProperties() if (m_visible) { if (m_imageChanged) { - doLoadCostume(); update(); m_imageChanged = false; } @@ -105,8 +124,16 @@ void RenderedTarget::updateProperties() setX(m_x); setY(m_y); setZ(m_z); + setWidth(m_width); + setHeight(m_height); setRotation(m_rotation); setTransformOriginPoint(QPointF(m_originX, m_originY)); + Q_ASSERT(m_maxSize > 0); + + if (!m_stageModel && (m_size > m_maxSize) && (m_maxSize != 0)) + setScale(m_size / m_maxSize); + else + setScale(1); if (m_newMirrorHorizontally != m_mirrorHorizontally) { m_mirrorHorizontally = m_newMirrorHorizontally; @@ -194,6 +221,28 @@ QNanoQuickItemPainter *RenderedTarget::createItemPainter() const return new TargetPainter(); } +void RenderedTarget::updateCostumeData() +{ + // Costume + m_costumeMutex.lock(); + + if (m_loadCostume) { + m_loadCostume = false; + m_imageChanged = true; + + if (m_costumeChanged) { + m_costumeChanged = false; + assert(m_costume); + + if (m_costume->dataFormat() == "svg") + m_svgRenderer.load(QByteArray::fromRawData(static_cast(m_costume->data()), m_costume->dataSize())); + } + } + + m_costumeMutex.unlock(); + doLoadCostume(); +} + void RenderedTarget::doLoadCostume() { m_costumeMutex.lock(); @@ -240,13 +289,9 @@ void RenderedTarget::paintSvg(QNanoPainter *painter) QSurface *oldSurface = context->surface(); context->makeCurrent(&surface); - const QRectF drawRect(0, 0, width(), height()); + const QRectF drawRect(0, 0, std::min(width(), m_maximumWidth), std::min(height(), m_maximumHeight)); const QSize drawRectSize = drawRect.size().toSize(); - /*QOpenGLFramebufferObjectFormat fboFormat; - fboFormat.setSamples(16); - fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);*/ - QOpenGLPaintDevice device(drawRectSize); QPainter qPainter; qPainter.begin(&device); @@ -262,15 +307,16 @@ void RenderedTarget::calculateSize(Target *target, double costumeWidth, double c { if (m_costume) { double bitmapRes = m_costume->bitmapResolution(); + m_maxSize = std::min(m_maximumWidth / costumeWidth, m_maximumHeight / costumeHeight); Sprite *sprite = dynamic_cast(target); if (sprite) { - double size = sprite->size(); - setWidth(costumeWidth * size / 100 / bitmapRes); - setHeight(costumeHeight * size / 100 / bitmapRes); + double clampedSize = std::min(m_size, m_maxSize); + m_width = costumeWidth * clampedSize / bitmapRes; + m_height = costumeHeight * clampedSize / bitmapRes; } else { - setWidth(costumeWidth / bitmapRes); - setHeight(costumeHeight / bitmapRes); + m_width = costumeWidth / bitmapRes; + m_height = costumeHeight / bitmapRes; } } } diff --git a/ScratchCPPGui/renderedtarget.h b/ScratchCPPGui/renderedtarget.h index 9b8dffe..992414c 100644 --- a/ScratchCPPGui/renderedtarget.h +++ b/ScratchCPPGui/renderedtarget.h @@ -70,6 +70,7 @@ class RenderedTarget : public IRenderedTarget QNanoQuickItemPainter *createItemPainter() const override; private: + void updateCostumeData(); void doLoadCostume(); void calculateSize(libscratchcpp::Target *target, double costumeWidth, double costumeHeight); @@ -82,16 +83,24 @@ class RenderedTarget : public IRenderedTarget QString m_bitmapUniqueKey; QMutex m_costumeMutex; QMutex mutex; + bool m_loadCostume = false; + bool m_costumeChanged = false; bool m_imageChanged = false; bool m_visible = true; + double m_size = 1; + double m_maxSize = 1; double m_x = 0; double m_y = 0; double m_z = 0; + double m_width = 0; + double m_height = 0; double m_rotation = 0; bool m_mirrorHorizontally = false; bool m_newMirrorHorizontally = false; double m_originX = 0; double m_originY = 0; + qreal m_maximumWidth = std::numeric_limits::infinity(); + qreal m_maximumHeight = std::numeric_limits::infinity(); }; } // namespace scratchcppgui diff --git a/test/renderedtarget/renderedtarget_test.cpp b/test/renderedtarget/renderedtarget_test.cpp index 55dc454..d8aa3ec 100644 --- a/test/renderedtarget/renderedtarget_test.cpp +++ b/test/renderedtarget/renderedtarget_test.cpp @@ -130,8 +130,8 @@ TEST_F(RenderedTargetTest, LoadAndUpdateProperties) ASSERT_EQ(target.transformOriginPoint(), QPointF(3.4, 9.7)); target.updateProperties(); - ASSERT_EQ(target.width(), 14.3); - ASSERT_EQ(target.height(), 5.8); + ASSERT_EQ(target.width(), 5.7592); + ASSERT_EQ(target.height(), 8.6388); ASSERT_EQ(std::round(target.x() * 100) / 100, 237.18); ASSERT_EQ(std::round(target.y() * 100) / 100, -100.93); ASSERT_EQ(target.z(), 3); @@ -197,6 +197,41 @@ TEST_F(RenderedTargetTest, LoadJpegCostume) ASSERT_TRUE(target.bitmapUniqueKey().toStdString().empty()); target.bitmapBuffer()->close(); + EngineMock engine; + SpriteModel spriteModel; + Sprite sprite; + sprite.setSize(196.5); + spriteModel.init(&sprite); + + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + target.setEngine(&engine); + target.setSpriteModel(&spriteModel); + target.loadProperties(); + 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()); + target.bitmapBuffer()->close(); + target.bitmapBuffer()->open(QBuffer::WriteOnly); // clear the buffer + target.bitmapBuffer()->close(); + + StageModel stageModel; + Stage stage; + + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + target.setSpriteModel(nullptr); + target.setStageModel(&stageModel); + target.loadProperties(); + 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()); + target.bitmapBuffer()->close(); + target.updateProperties(); ASSERT_FALSE(target.isSvg()); ASSERT_FALSE(target.bitmapBuffer()->isOpen()); @@ -223,6 +258,41 @@ TEST_F(RenderedTargetTest, LoadPngCostume) ASSERT_TRUE(target.bitmapUniqueKey().toStdString().empty()); target.bitmapBuffer()->close(); + EngineMock engine; + SpriteModel spriteModel; + Sprite sprite; + sprite.setSize(196.5); + spriteModel.init(&sprite); + + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + target.setEngine(&engine); + target.setSpriteModel(&spriteModel); + target.loadProperties(); + 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()); + target.bitmapBuffer()->close(); + target.bitmapBuffer()->open(QBuffer::WriteOnly); // clear the buffer + target.bitmapBuffer()->close(); + + StageModel stageModel; + Stage stage; + + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + target.setSpriteModel(nullptr); + target.setStageModel(&stageModel); + target.loadProperties(); + 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()); + target.bitmapBuffer()->close(); + target.updateProperties(); ASSERT_FALSE(target.isSvg()); ASSERT_FALSE(target.bitmapBuffer()->isOpen()); @@ -234,14 +304,53 @@ TEST_F(RenderedTargetTest, LoadPngCostume) 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"); - Costume costume("", "abc", "svg"); - costume.setData(str.size(), static_cast(const_cast(str.c_str()))); - costume.setBitmapResolution(3); + 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); + target.setSpriteModel(&model); - target.loadCostume(&costume); + target.loadCostume(costume.get()); + 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(); + + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + target.loadProperties(); ASSERT_TRUE(target.isSvg()); ASSERT_FALSE(target.bitmapBuffer()->isOpen()); target.bitmapBuffer()->open(QBuffer::ReadOnly); @@ -256,6 +365,51 @@ TEST_F(RenderedTargetTest, LoadSvgCostume) ASSERT_TRUE(target.bitmapBuffer()->readAll().toStdString().empty()); ASSERT_TRUE(target.bitmapUniqueKey().toStdString().empty()); target.bitmapBuffer()->close(); + + ASSERT_EQ(std::round(target.width() * 100) / 100, maxWidth); + ASSERT_EQ(std::round(target.height() * 100) / 100, maxHeight); + 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 + sprite.setSize(maxSize * 250); + + target.loadCostume(costume.get()); + 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(); + + EXPECT_CALL(engine, stageWidth()).WillOnce(Return(480)); + EXPECT_CALL(engine, stageHeight()).WillOnce(Return(360)); + target.loadProperties(); + 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(); + + target.updateProperties(); + 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, maxWidth); + ASSERT_EQ(std::round(target.height() * 100) / 100, maxHeight); + ASSERT_EQ(std::round(target.scale() * 100) / 100, 2.5); + 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_F(RenderedTargetTest, PaintSvg) @@ -265,15 +419,20 @@ TEST_F(RenderedTargetTest, PaintSvg) 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.loadProperties(); target.updateProperties(); // Create OpenGL context diff --git a/thirdparty/libqnanopainter/qnanoquickitempainter.cpp b/thirdparty/libqnanopainter/qnanoquickitempainter.cpp index 170e344..a2041dd 100644 --- a/thirdparty/libqnanopainter/qnanoquickitempainter.cpp +++ b/thirdparty/libqnanopainter/qnanoquickitempainter.cpp @@ -154,6 +154,7 @@ 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));