QskArcRenderer - work in progress, QskArcRenderNode added
This commit is contained in:
parent
49e1d54724
commit
f6845c709c
|
@ -87,16 +87,18 @@ ArcPage::ArcPage( QQuickItem* parent )
|
||||||
arc->setSpanAngle( 270.0 );
|
arc->setSpanAngle( 270.0 );
|
||||||
arc->setThickness( 10.0 );
|
arc->setThickness( 10.0 );
|
||||||
|
|
||||||
arc->setFillColor( Qt::darkRed );
|
arc->setFillColor( Qt::yellow );
|
||||||
|
|
||||||
arc->setBorderWidth( 0 );
|
arc->setBorderWidth( 2.0 );
|
||||||
arc->setBorderColor( Qt::darkYellow );
|
arc->setBorderColor( Qt::darkBlue );
|
||||||
|
|
||||||
|
#if 0
|
||||||
arc->setShadowColor( Qt::black );
|
arc->setShadowColor( Qt::black );
|
||||||
arc->setSpreadRadius( 0.0 );
|
arc->setSpreadRadius( 0.0 );
|
||||||
arc->setBlurRadius( 4.0 );
|
arc->setBlurRadius( 4.0 );
|
||||||
arc->setOffsetX( 2.0 );
|
arc->setOffsetX( 2.0 );
|
||||||
arc->setOffsetY( 2.0 );
|
arc->setOffsetY( 2.0 );
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
auto panel = new ControlPanel( arc );
|
auto panel = new ControlPanel( arc );
|
||||||
|
|
|
@ -102,6 +102,7 @@ list(APPEND SOURCES
|
||||||
list(APPEND HEADERS
|
list(APPEND HEADERS
|
||||||
nodes/QskArcNode.h
|
nodes/QskArcNode.h
|
||||||
nodes/QskArcRenderer.h
|
nodes/QskArcRenderer.h
|
||||||
|
nodes/QskArcRenderNode.h
|
||||||
nodes/QskArcShadowNode.h
|
nodes/QskArcShadowNode.h
|
||||||
nodes/QskBasicLinesNode.h
|
nodes/QskBasicLinesNode.h
|
||||||
nodes/QskBoxNode.h
|
nodes/QskBoxNode.h
|
||||||
|
@ -143,6 +144,7 @@ list(APPEND PRIVATE_HEADERS
|
||||||
list(APPEND SOURCES
|
list(APPEND SOURCES
|
||||||
nodes/QskArcNode.cpp
|
nodes/QskArcNode.cpp
|
||||||
nodes/QskArcRenderer.cpp
|
nodes/QskArcRenderer.cpp
|
||||||
|
nodes/QskArcRenderNode.cpp
|
||||||
nodes/QskArcShadowNode.cpp
|
nodes/QskArcShadowNode.cpp
|
||||||
nodes/QskBasicLinesNode.cpp
|
nodes/QskBasicLinesNode.cpp
|
||||||
nodes/QskBoxNode.cpp
|
nodes/QskBoxNode.cpp
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#include "QskArcNode.h"
|
#include "QskArcNode.h"
|
||||||
#include "QskArcMetrics.h"
|
#include "QskArcMetrics.h"
|
||||||
#include "QskArcShadowNode.h"
|
#include "QskArcShadowNode.h"
|
||||||
|
#include "QskArcRenderNode.h"
|
||||||
#include "QskMargins.h"
|
#include "QskMargins.h"
|
||||||
#include "QskGradient.h"
|
#include "QskGradient.h"
|
||||||
#include "QskShapeNode.h"
|
#include "QskShapeNode.h"
|
||||||
|
@ -13,9 +14,18 @@
|
||||||
#include "QskSGNode.h"
|
#include "QskSGNode.h"
|
||||||
#include "QskShadowMetrics.h"
|
#include "QskShadowMetrics.h"
|
||||||
|
|
||||||
#include <qpen.h>
|
|
||||||
#include <qpainterpath.h>
|
#include <qpainterpath.h>
|
||||||
|
|
||||||
|
#define ARC_RENDERER
|
||||||
|
|
||||||
|
#ifdef ARC_RENDERER
|
||||||
|
using BorderNode = QskArcRenderNode;
|
||||||
|
#else
|
||||||
|
#include <qpen.h>
|
||||||
|
using BorderNode = QskStrokeNode;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
enum NodeRole
|
enum NodeRole
|
||||||
|
@ -107,7 +117,7 @@ void QskArcNode::setArcData( const QRectF& rect, const QskArcMetrics& arcMetrics
|
||||||
auto fillNode = static_cast< QskShapeNode* >(
|
auto fillNode = static_cast< QskShapeNode* >(
|
||||||
QskSGNode::findChildNode( this, FillRole ) );
|
QskSGNode::findChildNode( this, FillRole ) );
|
||||||
|
|
||||||
auto borderNode = static_cast< QskStrokeNode* >(
|
auto borderNode = static_cast< BorderNode* >(
|
||||||
QskSGNode::findChildNode( this, BorderRole ) );
|
QskSGNode::findChildNode( this, BorderRole ) );
|
||||||
|
|
||||||
const auto arcRect = qskEffectiveRect( rect, borderWidth );
|
const auto arcRect = qskEffectiveRect( rect, borderWidth );
|
||||||
|
@ -173,14 +183,19 @@ void QskArcNode::setArcData( const QRectF& rect, const QskArcMetrics& arcMetrics
|
||||||
{
|
{
|
||||||
if ( borderNode == nullptr )
|
if ( borderNode == nullptr )
|
||||||
{
|
{
|
||||||
borderNode = new QskStrokeNode;
|
borderNode = new BorderNode;
|
||||||
QskSGNode::setNodeRole( borderNode, BorderRole );
|
QskSGNode::setNodeRole( borderNode, BorderRole );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ARC_RENDERER
|
||||||
|
borderNode->updateNode( arcRect, metricsArc, borderWidth,
|
||||||
|
borderColor, gradient );
|
||||||
|
#else
|
||||||
QPen pen( borderColor, borderWidth );
|
QPen pen( borderColor, borderWidth );
|
||||||
pen.setCapStyle( Qt::FlatCap );
|
pen.setCapStyle( Qt::FlatCap );
|
||||||
|
|
||||||
borderNode->updateNode( path, QTransform(), pen );
|
borderNode->updateNode( path, QTransform(), pen );
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -188,7 +203,7 @@ void QskArcNode::setArcData( const QRectF& rect, const QskArcMetrics& arcMetrics
|
||||||
borderNode = nullptr;
|
borderNode = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
qskUpdateChildren(this, ShadowRole, shadowNode);
|
qskUpdateChildren( this, ShadowRole, shadowNode );
|
||||||
qskUpdateChildren(this, FillRole, fillNode);
|
qskUpdateChildren( this, FillRole, fillNode );
|
||||||
qskUpdateChildren(this, BorderRole, borderNode);
|
qskUpdateChildren( this, BorderRole, borderNode );
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* QSkinny - Copyright (C) The authors
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
#include "QskArcRenderNode.h"
|
||||||
|
#include "QskGradient.h"
|
||||||
|
#include "QskArcRenderer.h"
|
||||||
|
#include "QskArcMetrics.h"
|
||||||
|
#include "QskGradient.h"
|
||||||
|
#include "QskSGNode.h"
|
||||||
|
|
||||||
|
QSK_QT_PRIVATE_BEGIN
|
||||||
|
#include <private/qsgnode_p.h>
|
||||||
|
QSK_QT_PRIVATE_END
|
||||||
|
|
||||||
|
#if 1
|
||||||
|
#include <qsgvertexcolormaterial.h>
|
||||||
|
#include <qglobalstatic.h>
|
||||||
|
// deriving from QskFillNode:TODO ...
|
||||||
|
Q_GLOBAL_STATIC( QSGVertexColorMaterial, qskMaterialColorVertex )
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class QskArcRenderNodePrivate final : public QSGGeometryNodePrivate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QskArcRenderNodePrivate()
|
||||||
|
: geometry( QSGGeometry::defaultAttributes_ColoredPoint2D(), 0 )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void resetValues()
|
||||||
|
{
|
||||||
|
hash = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSGGeometry geometry;
|
||||||
|
QskHashValue hash = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
QskArcRenderNode::QskArcRenderNode()
|
||||||
|
: QSGGeometryNode( *new QskArcRenderNodePrivate )
|
||||||
|
{
|
||||||
|
Q_D( QskArcRenderNode );
|
||||||
|
|
||||||
|
setGeometry( &d->geometry );
|
||||||
|
|
||||||
|
setMaterial( qskMaterialColorVertex );
|
||||||
|
setFlag( QSGNode::OwnsMaterial, false );
|
||||||
|
}
|
||||||
|
|
||||||
|
void QskArcRenderNode::updateNode(
|
||||||
|
const QRectF& rect, const QskArcMetrics& metrics, qreal borderWidth,
|
||||||
|
const QColor& borderColor, const QskGradient& gradient )
|
||||||
|
{
|
||||||
|
Q_D( QskArcRenderNode );
|
||||||
|
|
||||||
|
bool visible = !( rect.isEmpty() || metrics.isNull() );
|
||||||
|
if ( visible )
|
||||||
|
{
|
||||||
|
visible = gradient.isVisible();
|
||||||
|
if ( !visible )
|
||||||
|
{
|
||||||
|
visible = ( borderWidth > 0.0 )
|
||||||
|
&& borderColor.isValid() && ( borderColor.alpha() > 0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !visible )
|
||||||
|
{
|
||||||
|
d->resetValues();
|
||||||
|
QskSGNode::resetGeometry( this );
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QskHashValue hash = 3496;
|
||||||
|
|
||||||
|
hash = qHashBits( &rect, sizeof( QRectF ), hash );
|
||||||
|
hash = qHash( borderWidth, hash );
|
||||||
|
hash = qHash( borderColor.rgba(), hash );
|
||||||
|
hash = metrics.hash( hash );
|
||||||
|
hash = gradient.hash( hash );
|
||||||
|
|
||||||
|
if ( hash != d->hash )
|
||||||
|
{
|
||||||
|
d->hash = hash;
|
||||||
|
|
||||||
|
QskArcRenderer::renderBorderGeometry(
|
||||||
|
rect, metrics, borderWidth, *geometry() );
|
||||||
|
|
||||||
|
markDirty( QSGNode::DirtyGeometry );
|
||||||
|
markDirty( QSGNode::DirtyMaterial );
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* QSkinny - Copyright (C) The authors
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
#ifndef QSK_ARC_RENDER_NODE_H
|
||||||
|
#define QSK_ARC_RENDER_NODE_H
|
||||||
|
|
||||||
|
#include "QskGlobal.h"
|
||||||
|
#include <qsgnode.h>
|
||||||
|
|
||||||
|
class QskGradient;
|
||||||
|
class QskArcMetrics;
|
||||||
|
|
||||||
|
class QskArcRenderNodePrivate;
|
||||||
|
|
||||||
|
class QSK_EXPORT QskArcRenderNode : public QSGGeometryNode
|
||||||
|
{
|
||||||
|
using Inherited = QSGGeometryNode;
|
||||||
|
|
||||||
|
public:
|
||||||
|
QskArcRenderNode();
|
||||||
|
|
||||||
|
void updateNode( const QRectF&, const QskArcMetrics&, qreal borderWidth,
|
||||||
|
const QColor& borderColor, const QskGradient& );
|
||||||
|
|
||||||
|
private:
|
||||||
|
Q_DECLARE_PRIVATE( QskArcRenderNode )
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -10,6 +10,10 @@
|
||||||
|
|
||||||
#include <qsggeometry.h>
|
#include <qsggeometry.h>
|
||||||
|
|
||||||
|
#if 1
|
||||||
|
#include <qdebug.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
static inline QskVertex::Line* qskAllocateLines(
|
static inline QskVertex::Line* qskAllocateLines(
|
||||||
QSGGeometry& geometry, int lineCount )
|
QSGGeometry& geometry, int lineCount )
|
||||||
{
|
{
|
||||||
|
@ -44,8 +48,75 @@ static inline int qskApproximatedCircumference( const QRectF& rect )
|
||||||
|
|
||||||
static inline int qskStepCount( const QRectF& rect )
|
static inline int qskStepCount( const QRectF& rect )
|
||||||
{
|
{
|
||||||
|
#if 0
|
||||||
|
const auto dist = 3.0;
|
||||||
|
#else
|
||||||
|
const auto dist = 20.0;
|
||||||
|
#endif
|
||||||
|
|
||||||
const int length = qskApproximatedCircumference( rect );
|
const int length = qskApproximatedCircumference( rect );
|
||||||
return std::max( 3, qCeil( length / 3.0 ) );
|
return std::max( 3, qCeil( length / dist ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class AngleIterator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
AngleIterator( qreal radians1, qreal radians2, int stepCount );
|
||||||
|
|
||||||
|
inline void operator++() { increment(); }
|
||||||
|
void increment();
|
||||||
|
|
||||||
|
inline double cos() const { return m_cos; }
|
||||||
|
inline double sin() const { return m_sin; }
|
||||||
|
|
||||||
|
inline int step() const { return m_stepIndex; }
|
||||||
|
inline int stepCount() const { return m_stepCount; }
|
||||||
|
inline bool isDone() const { return m_stepIndex > m_stepCount; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
double m_cos;
|
||||||
|
double m_sin;
|
||||||
|
|
||||||
|
int m_stepIndex;
|
||||||
|
|
||||||
|
int m_stepCount;
|
||||||
|
|
||||||
|
const double m_radians1;
|
||||||
|
const double m_radians2;
|
||||||
|
const double m_radiansStep;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline AngleIterator::AngleIterator( qreal radians1, qreal radians2, int stepCount )
|
||||||
|
: m_stepIndex( 0 )
|
||||||
|
, m_stepCount( stepCount )
|
||||||
|
, m_radians1( radians1 )
|
||||||
|
, m_radians2( radians2 )
|
||||||
|
, m_radiansStep( ( radians2 - radians1 ) / stepCount )
|
||||||
|
{
|
||||||
|
m_cos = qFastCos( radians1 );
|
||||||
|
m_sin = qFastSin( radians1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void AngleIterator::increment()
|
||||||
|
{
|
||||||
|
if ( ++m_stepIndex >= m_stepCount )
|
||||||
|
{
|
||||||
|
if ( m_stepIndex == m_stepCount )
|
||||||
|
{
|
||||||
|
m_cos = qFastCos( m_radians2 );
|
||||||
|
m_sin = qFastSin( m_radians2 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const auto radians = m_radians1 + m_stepIndex * m_radiansStep;
|
||||||
|
|
||||||
|
m_cos = qFastCos( radians );
|
||||||
|
m_sin = qFastSin( radians );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
|
@ -58,49 +129,106 @@ namespace
|
||||||
int fillCount() const;
|
int fillCount() const;
|
||||||
int borderCount() const;
|
int borderCount() const;
|
||||||
|
|
||||||
void setBorderLines( QskVertex::Line* ) const;
|
int setBorderLines( QskVertex::ColoredLine* ) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
int arcLineCount() const;
|
||||||
|
|
||||||
|
void setArcLines( QskVertex::ColoredLine*, int lineCount,
|
||||||
|
const QPointF&, const qreal width, const qreal height,
|
||||||
|
const qreal radians1, const qreal radians2,
|
||||||
|
qreal arcWidth, const QskVertex::Color ) const;
|
||||||
|
|
||||||
const QRectF& m_rect;
|
const QRectF& m_rect;
|
||||||
const QskArcMetrics& m_metrics;
|
const QskArcMetrics& m_metrics;
|
||||||
const qreal m_borderWidth;
|
const qreal m_borderWidth;
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
Stroker::Stroker( const QRectF& rect, const QskArcMetrics& metrics, qreal borderWidth )
|
Stroker::Stroker( const QRectF& rect,
|
||||||
: m_rect( rect )
|
const QskArcMetrics& metrics, qreal borderWidth )
|
||||||
, m_metrics( metrics )
|
: m_rect( rect )
|
||||||
, m_borderWidth( borderWidth )
|
, m_metrics( metrics )
|
||||||
{
|
, m_borderWidth( borderWidth )
|
||||||
Q_ASSERT( metrics.sizeMode() == Qt::AbsoluteSize );
|
|
||||||
}
|
|
||||||
|
|
||||||
int Stroker::fillCount() const
|
|
||||||
{
|
|
||||||
return 0; // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
int Stroker::borderCount() const
|
|
||||||
{
|
|
||||||
if ( m_metrics.isNull() )
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
int n = qskStepCount( m_rect );
|
|
||||||
if ( !m_metrics.isClosed() )
|
|
||||||
{
|
{
|
||||||
n = qCeil( n * qAbs( m_metrics.spanAngle() ) / 360.0 );
|
Q_ASSERT( metrics.sizeMode() == Qt::AbsoluteSize );
|
||||||
n += 1; // closing line
|
|
||||||
}
|
}
|
||||||
|
|
||||||
n *= 2; // inner/outer border
|
int Stroker::fillCount() const
|
||||||
n += 1; // dummy line connection inner/outer border
|
{
|
||||||
|
return 0; // TODO
|
||||||
|
}
|
||||||
|
|
||||||
return n;
|
int Stroker::arcLineCount() const
|
||||||
}
|
{
|
||||||
|
if ( m_metrics.isNull() )
|
||||||
|
return 0;
|
||||||
|
|
||||||
void Stroker::setBorderLines( QskVertex::Line* lines ) const
|
int n = qskStepCount( m_rect );
|
||||||
{
|
if ( !m_metrics.isClosed() )
|
||||||
Q_UNUSED( lines );
|
n = qCeil( n * qAbs( m_metrics.spanAngle() ) / 360.0 );
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Stroker::borderCount() const
|
||||||
|
{
|
||||||
|
if ( m_metrics.isNull() )
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return 2 * arcLineCount() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Stroker::setArcLines( QskVertex::ColoredLine* lines, int lineCount,
|
||||||
|
const QPointF& center, const qreal w, const qreal h,
|
||||||
|
const qreal radians1, const qreal radians2,
|
||||||
|
qreal arcWidth, const QskVertex::Color color ) const
|
||||||
|
{
|
||||||
|
const auto w2 = w - arcWidth;
|
||||||
|
const auto h2 = h - arcWidth;
|
||||||
|
|
||||||
|
auto l = lines;
|
||||||
|
|
||||||
|
for ( AngleIterator it( radians1, radians2, lineCount - 1 ); !it.isDone(); ++it )
|
||||||
|
{
|
||||||
|
const auto x1 = center.x() + w * it.cos();
|
||||||
|
const auto x2 = center.x() + w2 * it.cos();
|
||||||
|
|
||||||
|
const auto y1 = center.y() + h * it.sin();
|
||||||
|
const auto y2 = center.y() + h2 * it.sin();
|
||||||
|
|
||||||
|
l++->setLine( x1, y1, x2, y2, color );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( l - lines != lineCount )
|
||||||
|
qWarning() << lineCount << "->" << l - lines;
|
||||||
|
Q_ASSERT( l - lines == lineCount );
|
||||||
|
}
|
||||||
|
|
||||||
|
int Stroker::setBorderLines( QskVertex::ColoredLine* lines ) const
|
||||||
|
{
|
||||||
|
const auto center = m_rect.center();
|
||||||
|
|
||||||
|
const qreal radians1 = qDegreesToRadians( m_metrics.startAngle() );
|
||||||
|
const qreal radians2 = qDegreesToRadians( m_metrics.endAngle() );
|
||||||
|
|
||||||
|
const int n = arcLineCount();
|
||||||
|
|
||||||
|
const QskVertex::Color color( QColor( Qt::darkBlue ) );
|
||||||
|
|
||||||
|
auto w = 0.5 * m_rect.width();
|
||||||
|
auto h = 0.5 * m_rect.height();
|
||||||
|
|
||||||
|
setArcLines( lines, n, center, w, h, radians1, radians2, m_borderWidth, color );
|
||||||
|
|
||||||
|
w -= m_metrics.thickness() - m_borderWidth;
|
||||||
|
h -= m_metrics.thickness() - m_borderWidth;
|
||||||
|
|
||||||
|
setArcLines( lines + n + 1, n, center, w, h, radians2, radians1, m_borderWidth, color );
|
||||||
|
|
||||||
|
lines[n] = { lines[n - 1].p2, lines[n + 1].p1 };
|
||||||
|
|
||||||
|
return 2 * n + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void QskArcRenderer::renderBorderGeometry( const QRectF& rect,
|
void QskArcRenderer::renderBorderGeometry( const QRectF& rect,
|
||||||
|
@ -110,9 +238,17 @@ void QskArcRenderer::renderBorderGeometry( const QRectF& rect,
|
||||||
|
|
||||||
Stroker stroker( rect, metrics, borderWidth );
|
Stroker stroker( rect, metrics, borderWidth );
|
||||||
|
|
||||||
const auto lines = qskAllocateLines( geometry, stroker.borderCount() );
|
const auto lineCount = stroker.borderCount();
|
||||||
|
|
||||||
|
const auto lines = qskAllocateColoredLines( geometry, lineCount );
|
||||||
if ( lines )
|
if ( lines )
|
||||||
stroker.setBorderLines( lines );
|
{
|
||||||
|
const auto effectiveCount = stroker.setBorderLines( lines );
|
||||||
|
if ( lineCount != effectiveCount )
|
||||||
|
{
|
||||||
|
qWarning() << lineCount << effectiveCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void QskArcRenderer::renderFillGeometry( const QRectF& rect,
|
void QskArcRenderer::renderFillGeometry( const QRectF& rect,
|
||||||
|
|
Loading…
Reference in New Issue