From 8316415c4c101336bfac7d41c581480c1f83a034 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 21 Sep 2021 14:57:54 +1000 Subject: [PATCH] Ensure geometry generator symbol layer works correctly outside of vector layer renderers Fixes #39159 --- .../qgsgeometrygeneratorsymbollayer.sip.in | 5 +- .../auto_generated/symbology/qgssymbol.sip.in | 5 +- src/core/symbology/qgsfillsymbol.cpp | 4 +- .../qgsgeometrygeneratorsymbollayer.cpp | 73 ++++++++++++++++--- .../qgsgeometrygeneratorsymbollayer.h | 5 +- src/core/symbology/qgslinesymbol.cpp | 4 +- src/core/symbology/qgsmarkersymbol.cpp | 12 ++- src/core/symbology/qgssymbol.cpp | 8 +- src/core/symbology/qgssymbol.h | 5 +- 9 files changed, 98 insertions(+), 23 deletions(-) diff --git a/python/core/auto_generated/symbology/qgsgeometrygeneratorsymbollayer.sip.in b/python/core/auto_generated/symbology/qgsgeometrygeneratorsymbollayer.sip.in index 7f74022b4864..d95f813daca7 100644 --- a/python/core/auto_generated/symbology/qgsgeometrygeneratorsymbollayer.sip.in +++ b/python/core/auto_generated/symbology/qgsgeometrygeneratorsymbollayer.sip.in @@ -120,7 +120,7 @@ This is a hybrid layer, it constructs its own geometry so it does not care about the geometry of its parents. %End - virtual void render( QgsSymbolRenderContext &context ); + void render( QgsSymbolRenderContext &context, const QPolygonF *points = 0, const QVector *rings = 0 ); %Docstring Will render this symbol layer using the context. In comparison to other symbols there is no geometry passed in, since @@ -130,6 +130,9 @@ context which is available to the evaluated expression. :param context: The rendering context which will be used to render and to construct a geometry. + +Since QGIS 3.22, the optional ``points`` and ``rings`` arguments can specify the original +points and rings in which are being rendered by the parent symbol. %End virtual void setColor( const QColor &color ); diff --git a/python/core/auto_generated/symbology/qgssymbol.sip.in b/python/core/auto_generated/symbology/qgssymbol.sip.in index 30cf381adb05..9e87c93e7c25 100644 --- a/python/core/auto_generated/symbology/qgssymbol.sip.in +++ b/python/core/auto_generated/symbology/qgssymbol.sip.in @@ -677,7 +677,7 @@ Retrieve a cloned list of all layers that make up this symbol. Ownership is transferred to the caller. %End - void renderUsingLayer( QgsSymbolLayer *layer, QgsSymbolRenderContext &context ); + void renderUsingLayer( QgsSymbolLayer *layer, QgsSymbolRenderContext &context, const QPolygonF *points = 0, const QVector *rings = 0 ); %Docstring Renders a context using a particular symbol layer without passing in a geometry. This is used as fallback, if the symbol being rendered is not @@ -686,6 +686,9 @@ called and will call the layer's rendering method anyway but the geometry passed to the layer will be empty. This is required for layers that generate their own geometry from other information in the rendering context. + +Since QGIS 3.22, the optional ``points`` and ``rings`` arguments can specify the original +points and rings in which are being rendered by the parent symbol. %End void renderVertexMarker( QPointF pt, QgsRenderContext &context, Qgis::VertexMarkerType currentVertexMarkerType, double currentVertexMarkerSize ); diff --git a/src/core/symbology/qgsfillsymbol.cpp b/src/core/symbology/qgsfillsymbol.cpp index 4db09f1f7f84..c44596c616c5 100644 --- a/src/core/symbology/qgsfillsymbol.cpp +++ b/src/core/symbology/qgsfillsymbol.cpp @@ -53,7 +53,7 @@ void QgsFillSymbol::renderPolygon( const QPolygonF &points, const QVectortype() == Qgis::SymbolType::Fill || symbolLayer->type() == Qgis::SymbolType::Line ) renderPolygonUsingLayer( symbolLayer, points, rings, symbolContext ); else - renderUsingLayer( symbolLayer, symbolContext ); + renderUsingLayer( symbolLayer, symbolContext, &points, rings ); } return; } @@ -70,7 +70,7 @@ void QgsFillSymbol::renderPolygon( const QPolygonF &points, const QVectortype() == Qgis::SymbolType::Fill || symbolLayer->type() == Qgis::SymbolType::Line ) renderPolygonUsingLayer( symbolLayer, points, rings, symbolContext ); else - renderUsingLayer( symbolLayer, symbolContext ); + renderUsingLayer( symbolLayer, symbolContext, &points, rings ); } } diff --git a/src/core/symbology/qgsgeometrygeneratorsymbollayer.cpp b/src/core/symbology/qgsgeometrygeneratorsymbollayer.cpp index 903f657fc1e6..8add604c0bba 100644 --- a/src/core/symbology/qgsgeometrygeneratorsymbollayer.cpp +++ b/src/core/symbology/qgsgeometrygeneratorsymbollayer.cpp @@ -18,6 +18,8 @@ #include "qgsmarkersymbol.h" #include "qgslinesymbol.h" #include "qgsfillsymbol.h" +#include "qgspolygon.h" + #include "qgsexpressioncontextutils.h" QgsGeometryGeneratorSymbolLayer::~QgsGeometryGeneratorSymbolLayer() = default; @@ -254,17 +256,68 @@ bool QgsGeometryGeneratorSymbolLayer::isCompatibleWithSymbol( QgsSymbol *symbol Q_UNUSED( symbol ) return true; } -void QgsGeometryGeneratorSymbolLayer::render( QgsSymbolRenderContext &context ) +void QgsGeometryGeneratorSymbolLayer::render( QgsSymbolRenderContext &context, const QPolygonF *points, const QVector *rings ) { if ( mRenderingFeature && mHasRenderedFeature ) return; - if ( context.feature() ) + QgsExpressionContext &expressionContext = context.renderContext().expressionContext(); + QgsFeature f = expressionContext.feature(); + + if ( !context.feature() && points ) { - QgsExpressionContext &expressionContext = context.renderContext().expressionContext(); + // oh dear, we don't have a feature to work from... but that's ok, we are probably being rendered as a plain old symbol! + // in this case we need to build up a feature which represents the points being rendered + QgsGeometry drawGeometry; - QgsFeature f = expressionContext.feature(); + // step 1 - convert points and rings to geometry + if ( points->size() == 1 ) + { + drawGeometry = QgsGeometry::fromPointXY( points->at( 0 ) ); + } + else if ( rings ) + { + // polygon + std::unique_ptr < QgsLineString > exterior( QgsLineString::fromQPolygonF( *points ) ); + std::unique_ptr< QgsPolygon > polygon = std::make_unique< QgsPolygon >(); + polygon->setExteriorRing( exterior.release() ); + for ( const QPolygonF &ring : *rings ) + { + polygon->addInteriorRing( QgsLineString::fromQPolygonF( ring ) ); + } + drawGeometry = QgsGeometry( std::move( polygon ) ); + } + else + { + // line + std::unique_ptr < QgsLineString > ring( QgsLineString::fromQPolygonF( *points ) ); + drawGeometry = QgsGeometry( std::move( ring ) ); + } + // step 2 - scale the draw geometry from PAINTER units to target units (e.g. millimeters) + const double scale = 1 / context.renderContext().convertToPainterUnits( 1, mUnits ); + const QTransform painterToTargetUnits = QTransform::fromScale( scale, scale ); + drawGeometry.transform( painterToTargetUnits ); + + // step 3 - set the feature to use the new scaled geometry, and inject it into the expression context + f.setGeometry( drawGeometry ); + QgsExpressionContextScope *generatorScope = new QgsExpressionContextScope(); + QgsExpressionContextScopePopper popper( expressionContext, generatorScope ); + generatorScope->setFeature( f ); + + // step 4 - evaluate the new generated geometry. + QgsGeometry geom = mExpression->evaluate( &expressionContext ).value(); + + // step 5 - transform geometry back from target units to MAP units. We transform to map units here + // as we'll ultimately be calling renderFeature, which excepts the feature has a geometry in map units. + // Here we also scale the transform by the target unit to painter units factor to reverse that conversion + QTransform mapToPixel = context.renderContext().mapToPixel().transform(); + mapToPixel.scale( scale, scale ); + geom.transform( mapToPixel.inverted() ); + f.setGeometry( geom ); + } + else if ( context.feature() ) + { switch ( mUnits ) { case QgsUnitTypes::RenderMapUnits: @@ -308,16 +361,16 @@ void QgsGeometryGeneratorSymbolLayer::render( QgsSymbolRenderContext &context ) break; } } + } - QgsExpressionContextScope *subSymbolExpressionContextScope = mSymbol->symbolRenderContext()->expressionContextScope(); + QgsExpressionContextScope *subSymbolExpressionContextScope = mSymbol->symbolRenderContext()->expressionContextScope(); - subSymbolExpressionContextScope->setFeature( f ); + subSymbolExpressionContextScope->setFeature( f ); - mSymbol->renderFeature( f, context.renderContext(), -1, context.selected() ); + mSymbol->renderFeature( f, context.renderContext(), -1, context.selected() ); - if ( mRenderingFeature ) - mHasRenderedFeature = true; - } + if ( mRenderingFeature ) + mHasRenderedFeature = true; } void QgsGeometryGeneratorSymbolLayer::setColor( const QColor &color ) diff --git a/src/core/symbology/qgsgeometrygeneratorsymbollayer.h b/src/core/symbology/qgsgeometrygeneratorsymbollayer.h index 9e3b0bac28fa..6337acfc6f7d 100644 --- a/src/core/symbology/qgsgeometrygeneratorsymbollayer.h +++ b/src/core/symbology/qgsgeometrygeneratorsymbollayer.h @@ -124,8 +124,11 @@ class CORE_EXPORT QgsGeometryGeneratorSymbolLayer : public QgsSymbolLayer * * \param context The rendering context which will be used to render and to * construct a geometry. + * + * Since QGIS 3.22, the optional \a points and \a rings arguments can specify the original + * points and rings in which are being rendered by the parent symbol. */ - virtual void render( QgsSymbolRenderContext &context ); + void render( QgsSymbolRenderContext &context, const QPolygonF *points = nullptr, const QVector *rings = nullptr ); void setColor( const QColor &color ) override; diff --git a/src/core/symbology/qgslinesymbol.cpp b/src/core/symbology/qgslinesymbol.cpp index 5b96dce373d0..4e5975b732b5 100644 --- a/src/core/symbology/qgslinesymbol.cpp +++ b/src/core/symbology/qgslinesymbol.cpp @@ -223,7 +223,7 @@ void QgsLineSymbol::renderPolyline( const QPolygonF &points, const QgsFeature *f renderPolylineUsingLayer( lineLayer, points, symbolContext ); } else - renderUsingLayer( symbolLayer, symbolContext ); + renderUsingLayer( symbolLayer, symbolContext, &points ); } return; } @@ -244,7 +244,7 @@ void QgsLineSymbol::renderPolyline( const QPolygonF &points, const QgsFeature *f } else { - renderUsingLayer( symbolLayer, symbolContext ); + renderUsingLayer( symbolLayer, symbolContext, &points ); } } diff --git a/src/core/symbology/qgsmarkersymbol.cpp b/src/core/symbology/qgsmarkersymbol.cpp index 0f43e9325681..2716b052c134 100644 --- a/src/core/symbology/qgsmarkersymbol.cpp +++ b/src/core/symbology/qgsmarkersymbol.cpp @@ -430,7 +430,11 @@ void QgsMarkerSymbol::renderPoint( QPointF point, const QgsFeature *f, QgsRender renderPointUsingLayer( markerLayer, point, symbolContext ); } else - renderUsingLayer( symbolLayer, symbolContext ); + { + QPolygonF points; + points.append( point ); + renderUsingLayer( symbolLayer, symbolContext, &points ); + } } return; } @@ -450,7 +454,11 @@ void QgsMarkerSymbol::renderPoint( QPointF point, const QgsFeature *f, QgsRender renderPointUsingLayer( markerLayer, point, symbolContext ); } else - renderUsingLayer( symbolLayer, symbolContext ); + { + QPolygonF points; + points.append( point ); + renderUsingLayer( symbolLayer, symbolContext, &points ); + } } } diff --git a/src/core/symbology/qgssymbol.cpp b/src/core/symbology/qgssymbol.cpp index b7b899918fdf..e5be90f2a9b4 100644 --- a/src/core/symbology/qgssymbol.cpp +++ b/src/core/symbology/qgssymbol.cpp @@ -778,7 +778,7 @@ QgsSymbolLayerList QgsSymbol::cloneLayers() const return lst; } -void QgsSymbol::renderUsingLayer( QgsSymbolLayer *layer, QgsSymbolRenderContext &context ) +void QgsSymbol::renderUsingLayer( QgsSymbolLayer *layer, QgsSymbolRenderContext &context, const QPolygonF *points, const QVector *rings ) { Q_ASSERT( layer->type() == Qgis::SymbolType::Hybrid ); @@ -791,11 +791,11 @@ void QgsSymbol::renderUsingLayer( QgsSymbolLayer *layer, QgsSymbolRenderContext if ( effect && effect->enabled() ) { QgsEffectPainter p( context.renderContext(), effect ); - generatorLayer->render( context ); + generatorLayer->render( context, points, rings ); } else { - generatorLayer->render( context ); + generatorLayer->render( context, points, rings ); } } @@ -950,6 +950,8 @@ void QgsSymbol::renderFeature( const QgsFeature &feature, QgsRenderContext &cont clippingEnabled = false; } } + if ( context.extent().isEmpty() ) + clippingEnabled = false; mSymbolRenderContext->setGeometryPartCount( geom.constGet()->partCount() ); mSymbolRenderContext->setGeometryPartNum( 1 ); diff --git a/src/core/symbology/qgssymbol.h b/src/core/symbology/qgssymbol.h index 92f2528e8796..198f3718094d 100644 --- a/src/core/symbology/qgssymbol.h +++ b/src/core/symbology/qgssymbol.h @@ -691,8 +691,11 @@ class CORE_EXPORT QgsSymbol * geometry passed to the layer will be empty. * This is required for layers that generate their own geometry from other * information in the rendering context. + * + * Since QGIS 3.22, the optional \a points and \a rings arguments can specify the original + * points and rings in which are being rendered by the parent symbol. */ - void renderUsingLayer( QgsSymbolLayer *layer, QgsSymbolRenderContext &context ); + void renderUsingLayer( QgsSymbolLayer *layer, QgsSymbolRenderContext &context, const QPolygonF *points = nullptr, const QVector *rings = nullptr ); /** * Render editing vertex marker at specified point