You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
290 lines
10 KiB
290 lines
10 KiB
/*****************************************************************************
|
|
* Copyright (C) 2022 VLC authors and VideoLAN
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* ( at your option ) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
|
*****************************************************************************/
|
|
|
|
#include "qsgroundedrectangularimagenode.hpp"
|
|
|
|
#include <QSGTextureMaterial>
|
|
#include <QSGOpaqueTextureMaterial>
|
|
|
|
#include <QCache>
|
|
#include <QVector>
|
|
#include <QPainterPath>
|
|
|
|
template<class T>
|
|
T QSGRoundedRectangularImageNode::material_cast(QSGMaterial* const material)
|
|
{
|
|
#ifdef NDEBUG
|
|
return static_cast<T>(object);
|
|
#else
|
|
const auto ret = dynamic_cast<T>(material);
|
|
assert(ret); // incompatible material type
|
|
return ret;
|
|
#endif
|
|
}
|
|
|
|
QSGRoundedRectangularImageNode::QSGRoundedRectangularImageNode()
|
|
{
|
|
setFlags(QSGGeometryNode::OwnsMaterial |
|
|
QSGGeometryNode::OwnsOpaqueMaterial |
|
|
QSGGeometryNode::OwnsGeometry);
|
|
|
|
setMaterial(new QSGTextureMaterial);
|
|
setOpaqueMaterial(new QSGOpaqueTextureMaterial);
|
|
|
|
setSmooth(m_smooth);
|
|
|
|
// Useful for debugging:
|
|
#ifdef QSG_RUNTIME_DESCRIPTION
|
|
qsgnode_set_description(this, QStringLiteral("RoundedRectangularImage"));
|
|
#endif
|
|
}
|
|
|
|
QSGTextureMaterial *QSGRoundedRectangularImageNode::material() const
|
|
{
|
|
return material_cast<QSGTextureMaterial*>(QSGGeometryNode::material());
|
|
}
|
|
|
|
QSGOpaqueTextureMaterial *QSGRoundedRectangularImageNode::opaqueMaterial() const
|
|
{
|
|
return material_cast<QSGOpaqueTextureMaterial*>(QSGGeometryNode::opaqueMaterial());
|
|
}
|
|
|
|
void QSGRoundedRectangularImageNode::setSmooth(const bool smooth)
|
|
{
|
|
if (m_smooth == smooth)
|
|
return;
|
|
|
|
{
|
|
const enum QSGTexture::Filtering filtering = smooth ? QSGTexture::Linear : QSGTexture::Nearest;
|
|
material()->setFiltering(filtering);
|
|
opaqueMaterial()->setFiltering(filtering);
|
|
}
|
|
|
|
{
|
|
const enum QSGTexture::Filtering mipmapFiltering = smooth ? QSGTexture::Linear : QSGTexture::None;
|
|
material()->setMipmapFiltering(mipmapFiltering);
|
|
opaqueMaterial()->setMipmapFiltering(mipmapFiltering);
|
|
}
|
|
|
|
markDirty(QSGNode::DirtyMaterial);
|
|
}
|
|
|
|
void QSGRoundedRectangularImageNode::setTexture(const std::shared_ptr<QSGTexture>& texture)
|
|
{
|
|
assert(texture);
|
|
|
|
{
|
|
const bool wasAtlas = (!m_texture || m_texture->isAtlasTexture());
|
|
|
|
m_texture = texture;
|
|
|
|
// Unless we operate on atlas textures, it should be
|
|
// fine to not rebuild the geometry
|
|
if (wasAtlas || texture->isAtlasTexture())
|
|
rebuildGeometry(); // Texture coordinate mismatch
|
|
}
|
|
|
|
material()->setTexture(texture.get());
|
|
opaqueMaterial()->setTexture(texture.get());
|
|
|
|
markDirty(QSGNode::DirtyMaterial);
|
|
}
|
|
|
|
bool QSGRoundedRectangularImageNode::setShape(const Shape& shape)
|
|
{
|
|
if (m_shape == shape)
|
|
return false;
|
|
|
|
const bool ret = rebuildGeometry(shape);
|
|
|
|
if (ret)
|
|
m_shape = shape;
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool QSGRoundedRectangularImageNode::rebuildGeometry(const Shape& shape)
|
|
{
|
|
QSGGeometry* const geometry = this->geometry();
|
|
QSGGeometry* const rebuiltGeometry = rebuildGeometry(shape,
|
|
geometry,
|
|
m_texture->isAtlasTexture() ? m_texture.get()
|
|
: nullptr);
|
|
if (!rebuiltGeometry)
|
|
{
|
|
return false;
|
|
}
|
|
else if (rebuiltGeometry == geometry)
|
|
{
|
|
// Was able to reconstruct old geometry instance
|
|
markDirty(QSGNode::DirtyGeometry);
|
|
}
|
|
else
|
|
{
|
|
// - Dirty bit set implicitly
|
|
// - No need to remove the old geometry
|
|
setGeometry(rebuiltGeometry);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QSGGeometry* QSGRoundedRectangularImageNode::rebuildGeometry(const Shape& shape,
|
|
QSGGeometry* geometry,
|
|
const QSGTexture* const atlasTexture)
|
|
{
|
|
if (!shape.isValid())
|
|
return nullptr;
|
|
|
|
int vertexCount;
|
|
|
|
QVector<QPointF> *path;
|
|
std::unique_ptr<QVector<QPointF>> upPath;
|
|
|
|
if (qFuzzyIsNull(shape.radius))
|
|
{
|
|
// 4 vertices are needed to construct
|
|
// a rectangle using triangle strip.
|
|
vertexCount = 4;
|
|
path = nullptr; // unused
|
|
}
|
|
else
|
|
{
|
|
using SizePair = QPair<qreal, qreal>;
|
|
using ShapePair = QPair<SizePair, qreal>;
|
|
|
|
// We could cache QSGGeometry itself, but
|
|
// it would not be really useful for atlas
|
|
// textures.
|
|
static QCache<ShapePair, QVector<QPointF>> paths;
|
|
|
|
ShapePair key {{shape.rect.width(), shape.rect.height()}, {shape.radius}};
|
|
if (paths.contains(key))
|
|
{
|
|
// There is no cache manipulation after this point,
|
|
// so path is assumed to be valid within its scope
|
|
path = paths.object(key);
|
|
}
|
|
else
|
|
{
|
|
QPainterPath painterPath;
|
|
constexpr const short extrapolationFactor = 2;
|
|
painterPath.addRoundedRect(0, 0,
|
|
key.first.first * extrapolationFactor,
|
|
key.first.second * extrapolationFactor,
|
|
key.second * extrapolationFactor,
|
|
key.second * extrapolationFactor);
|
|
painterPath = painterPath.simplified();
|
|
|
|
upPath = std::make_unique<QVector<QPointF>>(painterPath.elementCount());
|
|
path = upPath.get();
|
|
|
|
const int elementCount = painterPath.elementCount();
|
|
for (int i = 0; i < elementCount; ++i)
|
|
{
|
|
// QPainterPath is not necessarily compatible with
|
|
// with GPU primitives. However, simplified painter path
|
|
// with rounded rectangle shape consists of painter path
|
|
// elements of types which can be drawn using primitives.
|
|
assert(painterPath.elementAt(i).type == QPainterPath::ElementType::MoveToElement
|
|
|| painterPath.elementAt(i).type == QPainterPath::ElementType::LineToElement);
|
|
|
|
// Symmetry based triangulation based on ordered painter path.
|
|
(*path)[i] = static_cast<QPointF>(painterPath.elementAt((i % 2) ? (i)
|
|
: (elementCount - i - 1))) / extrapolationFactor;
|
|
}
|
|
|
|
paths.insert(key, new QVector<QPointF>(*path));
|
|
}
|
|
|
|
vertexCount = path->count();
|
|
}
|
|
|
|
if (!geometry)
|
|
{
|
|
geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), vertexCount);
|
|
geometry->setIndexDataPattern(QSGGeometry::StaticPattern); // Is this necessary? Indexing is not used
|
|
geometry->setVertexDataPattern(QSGGeometry::StaticPattern);
|
|
geometry->setDrawingMode(QSGGeometry::DrawingMode::DrawTriangleStrip);
|
|
}
|
|
else
|
|
{
|
|
// Size check is implicitly done:
|
|
geometry->allocate(vertexCount);
|
|
|
|
// Assume the passed geometry is not a stray one.
|
|
// It is possible to check and create a new QSGGeometry
|
|
// if it is incompatible, but it should not be necessary.
|
|
// So instead, just pass QSGGeometry to this function that
|
|
// is either inherently compatible, or that is created by
|
|
// this function.
|
|
|
|
// These two are not required for compatibility,
|
|
// but lets still assert them for performance reasons.
|
|
assert(geometry->indexDataPattern() == QSGGeometry::StaticPattern);
|
|
assert(geometry->vertexDataPattern() == QSGGeometry::StaticPattern);
|
|
|
|
assert(geometry->drawingMode() == QSGGeometry::DrawingMode::DrawTriangleStrip);
|
|
assert(geometry->attributes() == QSGGeometry::defaultAttributes_TexturedPoint2D().attributes);
|
|
assert(geometry->sizeOfVertex() == QSGGeometry::defaultAttributes_TexturedPoint2D().stride);
|
|
}
|
|
|
|
QRectF texNormalSubRect;
|
|
if (atlasTexture)
|
|
{
|
|
// The texture might not be in the atlas, but it is okay.
|
|
texNormalSubRect = atlasTexture->normalizedTextureSubRect();
|
|
}
|
|
else
|
|
{
|
|
// In case no texture is given at all:
|
|
texNormalSubRect = {0.0, 0.0, 1.0, 1.0};
|
|
}
|
|
|
|
if (qFuzzyIsNull(shape.radius))
|
|
{
|
|
// Use the helper function to reconstruct the pure rectangular geometry:
|
|
QSGGeometry::updateTexturedRectGeometry(geometry, shape.rect, texNormalSubRect);
|
|
}
|
|
else
|
|
{
|
|
const auto mapToAtlasTexture = [texNormalSubRect] (const QPointF& nPoint) -> QPointF {
|
|
return {texNormalSubRect.x() + (texNormalSubRect.width() * nPoint.x()),
|
|
texNormalSubRect.y() + (texNormalSubRect.height() * nPoint.y())};
|
|
};
|
|
|
|
QSGGeometry::TexturedPoint2D* const points = geometry->vertexDataAsTexturedPoint2D();
|
|
|
|
const qreal dx = shape.rect.x();
|
|
const qreal dy = shape.rect.y();
|
|
for (int i = 0; i < geometry->vertexCount(); ++i)
|
|
{
|
|
const QPointF& pos = path->at(i);
|
|
QPointF tPos = {pos.x() / shape.rect.width(), pos.y() / shape.rect.height()};
|
|
if (atlasTexture)
|
|
tPos = mapToAtlasTexture(tPos);
|
|
|
|
points[i].set(pos.x() + dx, pos.y() + dy, tPos.x(), tPos.y());
|
|
}
|
|
}
|
|
|
|
geometry->markIndexDataDirty();
|
|
geometry->markVertexDataDirty();
|
|
|
|
return geometry;
|
|
}
|
|
|