Improve results when using path stroking

Use GEOS to do the path stroking
This commit is contained in:
Nyall Dawson 2024-05-31 14:59:18 +10:00
parent 4eb35e466d
commit 3ace4676fa
5 changed files with 129 additions and 54 deletions

View File

@ -11,6 +11,7 @@
class QgsGeometryPaintDevice: QPaintDevice
{
%Docstring(signature="appended")

View File

@ -11,6 +11,7 @@
class QgsGeometryPaintDevice: QPaintDevice
{
%Docstring(signature="appended")

View File

@ -48,8 +48,12 @@ QPaintEngine::Type QgsGeometryPaintEngine::type() const
return QPaintEngine::User;
}
void QgsGeometryPaintEngine::updateState( const QPaintEngineState & )
void QgsGeometryPaintEngine::updateState( const QPaintEngineState &state )
{
if ( mUsePathStroker && state.state().testFlag( QPaintEngine::DirtyFlag::DirtyPen ) )
{
mPen = state.pen();
}
}
void QgsGeometryPaintEngine::drawImage( const QRectF &, const QImage &, const QRectF &, Qt::ImageConversionFlags )
@ -320,12 +324,76 @@ void QgsGeometryPaintEngine::drawPolygon( const QPointF *points, int pointCount,
}
}
void QgsGeometryPaintEngine::addStrokedLine( const QgsLineString *line, double penWidth, Qgis::EndCapStyle endCapStyle, Qgis::JoinStyle joinStyle, double miterLimit, const QTransform *matrix )
{
QgsGeos geos( line );
std::unique_ptr< QgsAbstractGeometry > buffered( geos.buffer( penWidth / 2, mStrokedPathsSegments, endCapStyle, joinStyle, miterLimit ) );
if ( !buffered )
return;
if ( matrix )
buffered->transform( *matrix );
if ( QgsGeometryCollection *bufferedCollection = qgsgeometry_cast< QgsGeometryCollection * >( buffered.get() ) )
{
mGeometry.addGeometries( bufferedCollection->takeGeometries() );
}
else if ( buffered )
{
mGeometry.addGeometry( buffered.release() );
}
}
Qgis::EndCapStyle QgsGeometryPaintEngine::penStyleToCapStyle( Qt::PenCapStyle style )
{
switch ( style )
{
case Qt::FlatCap:
return Qgis::EndCapStyle::Flat;
case Qt::SquareCap:
return Qgis::EndCapStyle::Square;
case Qt::RoundCap:
return Qgis::EndCapStyle::Round;
case Qt::MPenCapStyle:
// undocumented?
break;
}
return Qgis::EndCapStyle::Round;
}
Qgis::JoinStyle QgsGeometryPaintEngine::penStyleToJoinStyle( Qt::PenJoinStyle style )
{
switch ( style )
{
case Qt::MiterJoin:
case Qt::SvgMiterJoin:
return Qgis::JoinStyle::Miter;
case Qt::BevelJoin:
return Qgis::JoinStyle::Bevel;
case Qt::RoundJoin:
return Qgis::JoinStyle::Round;
case Qt::MPenJoinStyle:
// undocumented?
break;
}
return Qgis::JoinStyle::Round;
}
// based on QPainterPath::toSubpathPolygons()
void addSubpathGeometries( QgsGeometryCollection &collection, const QPainterPath &path, const QTransform &matrix )
void QgsGeometryPaintEngine::addSubpathGeometries( const QPainterPath &path, const QTransform &matrix )
{
if ( path.isEmpty() )
return;
const bool transformIsIdentity = matrix.isIdentity();
const Qgis::EndCapStyle endCapStyle = penStyleToCapStyle( mPen.capStyle() );
const Qgis::JoinStyle joinStyle = penStyleToJoinStyle( mPen.joinStyle() );
const double penWidth = mPen.widthF() <= 0 ? 1 : mPen.widthF();
const double miterLimit = mPen.miterLimit();
QVector< double > currentX;
QVector< double > currentY;
const int count = path.elementCount();
@ -343,13 +411,21 @@ void addSubpathGeometries( QgsGeometryCollection &collection, const QPainterPath
if ( currentX.size() > 1 )
{
std::unique_ptr< QgsLineString > line = std::make_unique< QgsLineString >( currentX, currentY );
if ( line->isClosed() )
if ( mUsePathStroker )
{
addStrokedLine( line.get(), penWidth, endCapStyle, joinStyle, miterLimit, transformIsIdentity ? nullptr : &matrix );
}
else if ( line->isClosed() )
{
if ( !transformIsIdentity )
line->transform( matrix );
queuedPolygons.emplace_back( std::make_unique< QgsPolygon >( line.release() ) );
}
else
{
collection.addGeometry( line.release() );
if ( !transformIsIdentity )
line->transform( matrix );
mGeometry.addGeometry( line.release() );
}
}
currentX.resize( 0 );
@ -357,19 +433,15 @@ void addSubpathGeometries( QgsGeometryCollection &collection, const QPainterPath
currentX.reserve( 16 );
currentY.reserve( 16 );
double tx, ty;
matrix.map( e.x, e.y, &tx, &ty );
currentX << tx;
currentY << ty;
currentX << e.x;
currentY << e.y;
break;
}
case QPainterPath::LineToElement:
{
double tx, ty;
matrix.map( e.x, e.y, &tx, &ty );
currentX << tx;
currentY << ty;
currentX << e.x;
currentY << e.y;
break;
}
@ -381,30 +453,18 @@ void addSubpathGeometries( QgsGeometryCollection &collection, const QPainterPath
const double x1 = path.elementAt( i - 1 ).x;
const double y1 = path.elementAt( i - 1 ).y;
double tx1, ty1;
matrix.map( x1, y1, &tx1, &ty1 );
double tx2, ty2;
matrix.map( e.x, e.y, &tx2, &ty2 );
const double x3 = path.elementAt( i + 1 ).x;
const double y3 = path.elementAt( i + 1 ).y;
double tx3, ty3;
matrix.map( x3, y3, &tx3, &ty3 );
const double x4 = path.elementAt( i + 2 ).x;
const double y4 = path.elementAt( i + 2 ).y;
double tx4, ty4;
matrix.map( x4, y4, &tx4, &ty4 );
// TODO -- we could likely reduce the number of segmented points here!
std::unique_ptr< QgsLineString> bezier( QgsLineString::fromBezierCurve(
QgsPoint( tx1, ty1 ),
QgsPoint( tx2, ty2 ),
QgsPoint( tx3, ty3 ),
QgsPoint( tx4, ty4 ) ) );
QgsPoint( x1, y1 ),
QgsPoint( e.x, e.y ),
QgsPoint( x3, y3 ),
QgsPoint( x4, y4 ) ) );
currentX << bezier->xVector();
currentY << bezier->yVector();
@ -421,20 +481,28 @@ void addSubpathGeometries( QgsGeometryCollection &collection, const QPainterPath
if ( currentX.size() > 1 )
{
std::unique_ptr< QgsLineString > line = std::make_unique< QgsLineString >( currentX, currentY );
if ( line->isClosed() )
if ( mUsePathStroker )
{
addStrokedLine( line.get(), penWidth, endCapStyle, joinStyle, miterLimit, transformIsIdentity ? nullptr : &matrix );
}
else if ( line->isClosed() )
{
if ( !transformIsIdentity )
line->transform( matrix );
queuedPolygons.emplace_back( std::make_unique< QgsPolygon >( line.release() ) );
}
else
{
collection.addGeometry( line.release() );
if ( !transformIsIdentity )
line->transform( matrix );
mGeometry.addGeometry( line.release() );
}
}
if ( queuedPolygons.empty() )
return;
collection.reserve( collection.numGeometries() + queuedPolygons.size() );
mGeometry.reserve( mGeometry.numGeometries() + queuedPolygons.size() );
QgsMultiPolygon tempMultiPolygon;
tempMultiPolygon.reserve( queuedPolygons.size() );
@ -451,24 +519,14 @@ void addSubpathGeometries( QgsGeometryCollection &collection, const QPainterPath
for ( auto it = g->const_parts_begin(); it != g->const_parts_end(); ++it )
{
collection.addGeometry( ( *it )->clone() );
mGeometry.addGeometry( ( *it )->clone() );
}
}
void QgsGeometryPaintEngine::drawPath( const QPainterPath &path )
{
QPainterPath realPath = path;
if ( mUsePathStroker )
{
QPen pen = painter()->pen();
QPainterPathStroker stroker( pen );
QPainterPath strokedPath = stroker.createStroke( path );
realPath = strokedPath;
}
QTransform transform = painter()->combinedTransform();
addSubpathGeometries( mGeometry, realPath, transform );
const QTransform transform = painter()->combinedTransform();
addSubpathGeometries( path, transform );
}
//

View File

@ -25,6 +25,8 @@
#include <QPaintEngine>
#include <memory>
class QgsLineString;
#ifndef SIP_RUN
/**
@ -82,8 +84,15 @@ class QgsGeometryPaintEngine: public QPaintEngine
private:
void addSubpathGeometries( const QPainterPath &path, const QTransform &matrix );
void addStrokedLine( const QgsLineString *line, double penWidth, Qgis::EndCapStyle endCapStyle, Qgis::JoinStyle joinStyle, double miterLimit, const QTransform *matrix );
static Qgis::EndCapStyle penStyleToCapStyle( Qt::PenCapStyle style );
static Qgis::JoinStyle penStyleToJoinStyle( Qt::PenJoinStyle style );
bool mUsePathStroker = false;
QPen mPen;
QgsGeometryCollection mGeometry;
int mStrokedPathsSegments = 8;
};
#endif

View File

@ -75,8 +75,10 @@ class TestQgsGeometryPaintDevice(QgisTestCase):
)
painter.end()
self.assertEqual(device.geometry().asWkt(2),
'GeometryCollection (LineString (5.5 10.7, 6.8 12.9),LineString (15.5 12.7, 3.8 42.9),LineString (-4 -1, 2 3))')
result = device.geometry()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (LineString (15.5 12.7, 3.8 42.9),LineString (5.5 10.7, 6.8 12.9),LineString (-4 -1, 2 3))')
self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
@ -104,8 +106,10 @@ class TestQgsGeometryPaintDevice(QgisTestCase):
painter.end()
self.assertEqual(device.geometry().asWkt(2),
'GeometryCollection (LineString (11 32.1, 13.6 38.7),LineString (31 38.1, 7.6 128.7),LineString (-8 -3, 4 9))')
result = device.geometry()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (LineString (31 38.1, 7.6 128.7),LineString (11 32.1, 13.6 38.7),LineString (-8 -3, 4 9))')
self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
39)
@ -134,8 +138,10 @@ class TestQgsGeometryPaintDevice(QgisTestCase):
painter.end()
self.assertEqual(device.geometry().asWkt(2),
'GeometryCollection (Polygon ((6.15 10.32, 7.45 12.52, 6.15 13.28, 4.85 11.08, 6.15 10.32)),Polygon ((16.2 12.97, 4.5 43.17, 3.1 42.63, 14.8 12.43, 16.2 12.97)),Polygon ((-3.58 -1.62, 2.42 2.38, 1.58 3.62, -4.42 -0.38, -3.58 -1.62)))')
result = device.geometry()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (Polygon ((4.85 11.08, 6.15 13.28, 7.45 12.52, 6.15 10.32, 4.85 11.08)),Polygon ((3.1 42.63, 4.5 43.17, 16.2 12.97, 14.8 12.43, 3.1 42.63)),Polygon ((-4.42 -0.38, 1.58 3.62, 2.42 2.38, -3.58 -1.62, -4.42 -0.38)))')
self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
@ -378,7 +384,7 @@ class TestQgsGeometryPaintDevice(QgisTestCase):
result = device.geometry().clone()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (Polygon ((4.85 11.08, 6.15 13.28, 6.82 13.65, 15.52 13.45, 15.65 11.96, 5.65 9.96, 4.85 11.08),(8.71 12.11, 15.48 11.95, 15.5 12.7, 15.35 13.44, 8.71 12.11),(6.78 12.15, 7.22 12.14, 7.45 12.52, 6.8 12.9, 6.78 12.15),(5.35 11.44, 5.5 10.7, 6.15 10.32, 7 11.76, 5.35 11.44),(7 11.76, 8.71 12.11, 7.22 12.14, 7 11.76)),Polygon ((13.29 11.24, 21.29 35.24, 22.71 34.76, 14.71 10.76, 13.29 11.24)),Polygon ((-4.42 -0.38, 1.58 3.62, 2.42 2.38, -3.58 -1.62, -4.42 -0.38)))')
'GeometryCollection (Polygon ((4.85 11.08, 6.15 13.28, 6.82 13.65, 15.52 13.45, 15.65 11.96, 5.65 9.96, 4.85 11.08),(7 11.76, 8.71 12.11, 7.22 12.14, 7 11.76)),Polygon ((13.29 11.24, 21.29 35.24, 22.71 34.76, 14.71 10.76, 13.29 11.24)),Polygon ((-4.42 -0.38, 1.58 3.62, 2.42 2.38, -3.58 -1.62, -4.42 -0.38)))')
self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
@ -409,7 +415,7 @@ class TestQgsGeometryPaintDevice(QgisTestCase):
result = device.geometry().clone()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (Polygon ((9.71 33.24, 12.31 39.84, 13.63 40.95, 31.03 40.35, 31.29 35.89, 11.29 29.89, 9.71 33.24),(17.41 36.32, 30.97 35.85, 31 38.1, 30.71 40.31, 17.41 36.32),(13.57 36.45, 14.44 36.42, 14.89 37.56, 13.6 38.7, 13.57 36.45),(10.71 34.31, 11 32.1, 12.29 30.96, 14 35.29, 10.71 34.31),(14 35.29, 17.41 36.32, 14.44 36.42, 14 35.29)),Polygon ((26.58 33.71, 42.58 105.71, 45.42 104.29, 29.42 32.29, 26.58 33.71)),Polygon ((-8.83 -1.13, 3.17 10.87, 4.83 7.13, -7.17 -4.87, -8.83 -1.13)))')
'GeometryCollection (Polygon ((9.71 33.24, 12.31 39.84, 13.63 40.95, 31.03 40.35, 31.29 35.89, 11.29 29.89, 9.71 33.24),(14 35.29, 17.41 36.32, 14.44 36.42, 14 35.29)),Polygon ((26.58 33.71, 42.58 105.71, 45.42 104.29, 29.42 32.29, 26.58 33.71)),Polygon ((-8.83 -1.13, 3.17 10.87, 4.83 7.13, -7.17 -4.87, -8.83 -1.13)))')
self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
54)
@ -489,7 +495,7 @@ class TestQgsGeometryPaintDevice(QgisTestCase):
result = device.geometry().clone()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (Polygon ((4.82 10.39, 5.35 11.44, 15.35 13.44, 16.23 12.51, 13.8 3.2, 13.7 2.98, 13.61 2.85, 13.52 2.72, 13.43 2.6, 13.34 2.47, 13.25 2.35, 13.16 2.23, 13.07 2.11, 12.97 1.99, 12.87 1.87, 12.78 1.76, 12.68 1.64, 12.58 1.53, 12.48 1.42, 12.38 1.31, 12.28 1.2, 12.18 1.09, 12.07 0.99, 11.97 0.88, 11.86 0.78, 11.76 0.68, 11.65 0.58, 11.54 0.49, 11.43 0.39, 11.32 0.3, 11.21 0.21, 11.1 0.12, 10.99 0.03, 10.88 -0.05, 10.77 -0.13, 10.66 -0.21, 9.55 0.09, 4.82 10.39),(6.58 10.15, 10.51 1.58, 10.57 1.62, 10.66 1.71, 10.75 1.79, 10.85 1.88, 10.94 1.96, 11.03 2.05, 11.12 2.14, 11.2 2.23, 11.29 2.33, 11.38 2.42, 11.46 2.52, 11.55 2.62, 11.63 2.72, 11.72 2.82, 11.8 2.92, 11.88 3.03, 11.97 3.13, 12.05 3.24, 12.13 3.35, 12.21 3.46, 12.29 3.58, 12.37 3.69, 12.38 3.71, 14.47 11.73, 6.58 10.15),(9.8 1.02, 10.23 0.4, 10.91 0.72, 10.51 1.58, 10.48 1.54, 10.38 1.46, 10.29 1.39, 10.19 1.31, 10.09 1.23, 10 1.16, 9.9 1.09, 9.8 1.02),(14.47 11.73, 15.65 11.96, 15.5 12.7, 14.77 12.89, 14.47 11.73),(12.35 3.58, 13.07 3.39, 12.45 3.81, 12.38 3.71, 12.35 3.58),(5.5 10.7, 5.65 9.96, 6.58 10.15, 6.18 11.01, 5.5 10.7)))')
'GeometryCollection (Polygon ((4.82 10.39, 5.35 11.44, 15.35 13.44, 16.23 12.51, 13.8 3.2, 13.69 2.97, 13.61 2.85, 13.61 2.85, 13.52 2.72, 13.52 2.72, 13.43 2.6, 13.43 2.59, 13.34 2.47, 13.34 2.47, 13.25 2.35, 13.25 2.34, 13.16 2.23, 13.16 2.22, 13.07 2.11, 13.06 2.1, 12.97 1.99, 12.97 1.98, 12.88 1.87, 12.87 1.87, 12.78 1.76, 12.78 1.75, 12.69 1.64, 12.68 1.64, 12.59 1.53, 12.58 1.52, 12.49 1.42, 12.48 1.41, 12.39 1.31, 12.38 1.3, 12.29 1.2, 12.28 1.2, 12.19 1.1, 12.18 1.09, 12.08 0.99, 12.08 0.99, 11.98 0.89, 11.97 0.88, 11.87 0.79, 11.87 0.78, 11.77 0.69, 11.76 0.68, 11.66 0.59, 11.66 0.59, 11.56 0.5, 11.55 0.49, 11.45 0.4, 11.44 0.4, 11.34 0.31, 11.33 0.3, 11.23 0.22, 11.22 0.21, 11.12 0.13, 11.11 0.12, 11 0.04, 11 0.04, 10.89 -0.04, 10.88 -0.05, 10.78 -0.13, 10.77 -0.13, 10.66 -0.21, 9.55 0.09, 4.82 10.39),(6.58 10.15, 10.51 1.58, 10.56 1.62, 10.65 1.7, 10.75 1.79, 10.84 1.87, 10.93 1.96, 11.02 2.05, 11.11 2.14, 11.2 2.23, 11.29 2.33, 11.37 2.42, 11.46 2.52, 11.55 2.62, 11.63 2.72, 11.72 2.82, 11.8 2.93, 11.88 3.03, 11.97 3.14, 12.05 3.25, 12.13 3.36, 12.21 3.47, 12.29 3.58, 12.37 3.69, 12.38 3.71, 14.47 11.73, 6.58 10.15)))')
self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
@ -512,7 +518,7 @@ class TestQgsGeometryPaintDevice(QgisTestCase):
result = device.geometry().clone()
result.normalize()
self.assertEqual(result.asWkt(2),
'GeometryCollection (Polygon ((9.64 31.16, 10.71 34.31, 30.71 40.31, 32.45 37.53, 27.59 9.61, 27.39 8.93, 27.22 8.55, 27.04 8.17, 26.87 7.79, 26.69 7.42, 26.5 7.05, 26.32 6.69, 26.13 6.33, 25.94 5.97, 25.75 5.62, 25.56 5.27, 25.36 4.92, 25.16 4.58, 24.96 4.25, 24.76 3.92, 24.56 3.59, 24.35 3.27, 24.14 2.96, 23.93 2.65, 23.72 2.34, 23.51 2.04, 23.3 1.75, 23.08 1.46, 22.87 1.18, 22.65 0.9, 22.43 0.63, 22.21 0.36, 21.99 0.1, 21.77 -0.15, 21.54 -0.39, 21.32 -0.63, 19.09 0.27, 9.64 31.16),(13.16 30.45, 21.03 4.73, 21.14 4.87, 21.33 5.12, 21.51 5.37, 21.69 5.63, 21.87 5.89, 22.05 6.16, 22.23 6.43, 22.41 6.7, 22.58 6.99, 22.76 7.27, 22.93 7.56, 23.1 7.86, 23.27 8.16, 23.44 8.46, 23.6 8.77, 23.77 9.08, 23.93 9.4, 24.1 9.73, 24.26 10.06, 24.42 10.39, 24.58 10.73, 24.74 11.08, 24.76 11.12, 28.94 35.19, 13.16 30.45),(19.6 3.05, 20.46 1.21, 21.82 2.15, 21.03 4.73, 20.95 4.63, 20.76 4.39, 20.57 4.16, 20.38 3.93, 20.19 3.7, 19.99 3.48, 19.79 3.26, 19.6 3.05),(28.94 35.19, 31.29 35.89, 31 38.1, 29.55 38.67, 28.94 35.19),(24.69 10.75, 26.14 10.18, 24.89 11.43, 24.76 11.12, 24.69 10.75),(11 32.1, 11.29 29.89, 13.16 30.45, 12.36 33.04, 11 32.1)))')
'GeometryCollection (Polygon ((9.64 31.16, 10.71 34.31, 30.71 40.31, 32.45 37.53, 27.59 9.61, 27.39 8.92, 27.22 8.56, 27.21 8.54, 27.05 8.17, 27.04 8.15, 26.87 7.8, 26.86 7.78, 26.69 7.42, 26.68 7.4, 26.51 7.05, 26.5 7.03, 26.32 6.69, 26.31 6.67, 26.14 6.33, 26.12 6.31, 25.95 5.97, 25.94 5.95, 25.76 5.62, 25.75 5.6, 25.57 5.27, 25.55 5.25, 25.37 4.93, 25.36 4.91, 25.18 4.59, 25.16 4.57, 24.98 4.26, 24.97 4.24, 24.78 3.93, 24.77 3.91, 24.58 3.61, 24.56 3.59, 24.37 3.29, 24.36 3.27, 24.17 2.98, 24.15 2.96, 23.96 2.67, 23.95 2.65, 23.75 2.37, 23.74 2.35, 23.54 2.07, 23.52 2.05, 23.33 1.78, 23.31 1.76, 23.11 1.49, 23.1 1.47, 22.89 1.2, 22.88 1.19, 22.67 0.93, 22.66 0.91, 22.45 0.66, 22.44 0.64, 22.23 0.39, 22.22 0.37, 22.01 0.13, 21.99 0.11, 21.78 -0.13, 21.77 -0.15, 21.56 -0.38, 21.54 -0.4, 21.33 -0.62, 19.09 0.27, 9.64 31.16),(13.16 30.45, 21.03 4.73, 21.12 4.85, 21.31 5.1, 21.49 5.36, 21.67 5.62, 21.86 5.88, 22.04 6.15, 22.22 6.42, 22.39 6.7, 22.57 6.98, 22.75 7.27, 22.92 7.56, 23.09 7.86, 23.26 8.16, 23.43 8.47, 23.6 8.78, 23.77 9.09, 23.93 9.41, 24.1 9.74, 24.26 10.07, 24.42 10.4, 24.58 10.74, 24.74 11.08, 24.76 11.12, 28.94 35.19, 13.16 30.45)))')
self.assertEqual(
device.metric(QPaintDevice.PaintDeviceMetric.PdmWidth),
22)