diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py index 0f59f8308705..57b6e2b2d338 100644 --- a/python/PyQt6/core/auto_additions/qgis.py +++ b/python/PyQt6/core/auto_additions/qgis.py @@ -4772,6 +4772,9 @@ QgsRenderContext.AlwaysUseGlobalMasks = Qgis.RenderContextFlag.AlwaysUseGlobalMasks QgsRenderContext.AlwaysUseGlobalMasks.is_monkey_patched = True QgsRenderContext.AlwaysUseGlobalMasks.__doc__ = "When applying clipping paths for selective masking, always use global (\"entire map\") paths, instead of calculating local clipping paths per rendered feature. This results in considerably more complex vector exports in all current Qt versions. This flag only applies to vector map exports. \n.. versionadded:: 3.38" +QgsRenderContext.DisableSymbolClippingToExtent = Qgis.RenderContextFlag.DisableSymbolClippingToExtent +QgsRenderContext.DisableSymbolClippingToExtent.is_monkey_patched = True +QgsRenderContext.DisableSymbolClippingToExtent.__doc__ = "Force symbol clipping to map extent to be disabled in all situations. This will result in slower rendering, and should only be used in situations where the feature clipping is always undesirable. \n.. versionadded:: 3.40" Qgis.RenderContextFlag.__doc__ = """Flags which affect rendering operations. .. versionadded:: 3.22 @@ -4812,6 +4815,10 @@ .. versionadded:: 3.38 +* ``DisableSymbolClippingToExtent``: Force symbol clipping to map extent to be disabled in all situations. This will result in slower rendering, and should only be used in situations where the feature clipping is always undesirable. + + .. versionadded:: 3.40 + """ # -- diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in index 4de51989d532..6bc2f1968ecf 100644 --- a/python/PyQt6/core/auto_generated/qgis.sip.in +++ b/python/PyQt6/core/auto_generated/qgis.sip.in @@ -1530,6 +1530,7 @@ The development version SkipSymbolRendering, RecordProfile, AlwaysUseGlobalMasks, + DisableSymbolClippingToExtent, }; typedef QFlags RenderContextFlags; diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index db67ec7a24bb..aa83348f5aaa 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -4724,6 +4724,9 @@ QgsRenderContext.AlwaysUseGlobalMasks = Qgis.RenderContextFlag.AlwaysUseGlobalMasks QgsRenderContext.AlwaysUseGlobalMasks.is_monkey_patched = True QgsRenderContext.AlwaysUseGlobalMasks.__doc__ = "When applying clipping paths for selective masking, always use global (\"entire map\") paths, instead of calculating local clipping paths per rendered feature. This results in considerably more complex vector exports in all current Qt versions. This flag only applies to vector map exports. \n.. versionadded:: 3.38" +QgsRenderContext.DisableSymbolClippingToExtent = Qgis.RenderContextFlag.DisableSymbolClippingToExtent +QgsRenderContext.DisableSymbolClippingToExtent.is_monkey_patched = True +QgsRenderContext.DisableSymbolClippingToExtent.__doc__ = "Force symbol clipping to map extent to be disabled in all situations. This will result in slower rendering, and should only be used in situations where the feature clipping is always undesirable. \n.. versionadded:: 3.40" Qgis.RenderContextFlag.__doc__ = """Flags which affect rendering operations. .. versionadded:: 3.22 @@ -4764,6 +4767,10 @@ .. versionadded:: 3.38 +* ``DisableSymbolClippingToExtent``: Force symbol clipping to map extent to be disabled in all situations. This will result in slower rendering, and should only be used in situations where the feature clipping is always undesirable. + + .. versionadded:: 3.40 + """ # -- diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 8d685aaa33f7..7863f60204bb 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -1530,6 +1530,7 @@ The development version SkipSymbolRendering, RecordProfile, AlwaysUseGlobalMasks, + DisableSymbolClippingToExtent, }; typedef QFlags RenderContextFlags; diff --git a/src/core/layout/qgslayoutitempolygon.cpp b/src/core/layout/qgslayoutitempolygon.cpp index c101c7ceb639..e75cec3a1a6b 100644 --- a/src/core/layout/qgslayoutitempolygon.cpp +++ b/src/core/layout/qgslayoutitempolygon.cpp @@ -136,18 +136,23 @@ QgsFillSymbol *QgsLayoutItemPolygon::symbol() void QgsLayoutItemPolygon::_draw( QgsLayoutItemRenderContext &context, const QStyleOptionGraphicsItem * ) { + QgsRenderContext renderContext = context.renderContext(); + // symbol clipping messes with geometry generators used in the symbol for this item, and has no + // valid use here. See https://github.com/qgis/QGIS/issues/58909 + renderContext.setFlag( Qgis::RenderContextFlag::DisableSymbolClippingToExtent ); + //setup painter scaling to dots so that raster symbology is drawn to scale - const double scale = context.renderContext().convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); + const double scale = renderContext.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); const QTransform t = QTransform::fromScale( scale, scale ); const QVector rings; //empty QPainterPath polygonPath; polygonPath.addPolygon( mPolygon ); - mPolygonStyleSymbol->startRender( context.renderContext() ); + mPolygonStyleSymbol->startRender( renderContext ); mPolygonStyleSymbol->renderPolygon( polygonPath.toFillPolygon( t ), &rings, - nullptr, context.renderContext() ); - mPolygonStyleSymbol->stopRender( context.renderContext() ); + nullptr, renderContext ); + mPolygonStyleSymbol->stopRender( renderContext ); } void QgsLayoutItemPolygon::_readXmlStyle( const QDomElement &elmt, const QgsReadWriteContext &context ) diff --git a/src/core/layout/qgslayoutitempolyline.cpp b/src/core/layout/qgslayoutitempolyline.cpp index 73f0350b873c..8702eaa665ae 100644 --- a/src/core/layout/qgslayoutitempolyline.cpp +++ b/src/core/layout/qgslayoutitempolyline.cpp @@ -280,20 +280,25 @@ QString QgsLayoutItemPolyline::displayName() const void QgsLayoutItemPolyline::_draw( QgsLayoutItemRenderContext &context, const QStyleOptionGraphicsItem * ) { - const QgsScopedQPainterState painterState( context.renderContext().painter() ); + QgsRenderContext renderContext = context.renderContext(); + // symbol clipping messes with geometry generators used in the symbol for this item, and has no + // valid use here. See https://github.com/qgis/QGIS/issues/58909 + renderContext.setFlag( Qgis::RenderContextFlag::DisableSymbolClippingToExtent ); + + const QgsScopedQPainterState painterState( renderContext.painter() ); //setup painter scaling to dots so that raster symbology is drawn to scale - const double scale = context.renderContext().convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); + const double scale = renderContext.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); const QTransform t = QTransform::fromScale( scale, scale ); - mPolylineStyleSymbol->startRender( context.renderContext() ); - mPolylineStyleSymbol->renderPolyline( t.map( mPolygon ), nullptr, context.renderContext() ); - mPolylineStyleSymbol->stopRender( context.renderContext() ); + mPolylineStyleSymbol->startRender( renderContext ); + mPolylineStyleSymbol->renderPolyline( t.map( mPolygon ), nullptr, renderContext ); + mPolylineStyleSymbol->stopRender( renderContext ); // painter is scaled to dots, so scale back to layout units - context.renderContext().painter()->scale( context.renderContext().scaleFactor(), context.renderContext().scaleFactor() ); + renderContext.painter()->scale( renderContext.scaleFactor(), renderContext.scaleFactor() ); - drawStartMarker( context.renderContext().painter() ); - drawEndMarker( context.renderContext().painter() ); + drawStartMarker( renderContext.painter() ); + drawEndMarker( renderContext.painter() ); } void QgsLayoutItemPolyline::_readXmlStyle( const QDomElement &elmt, const QgsReadWriteContext &context ) diff --git a/src/core/layout/qgslayoutitemshape.cpp b/src/core/layout/qgslayoutitemshape.cpp index 04ac544b156f..90cf87c12547 100644 --- a/src/core/layout/qgslayoutitemshape.cpp +++ b/src/core/layout/qgslayoutitemshape.cpp @@ -196,17 +196,22 @@ bool QgsLayoutItemShape::accept( QgsStyleEntityVisitorInterface *visitor ) const void QgsLayoutItemShape::draw( QgsLayoutItemRenderContext &context ) { - QPainter *painter = context.renderContext().painter(); + QgsRenderContext renderContext = context.renderContext(); + // symbol clipping messes with geometry generators used in the symbol for this item, and has no + // valid use here. See https://github.com/qgis/QGIS/issues/58909 + renderContext.setFlag( Qgis::RenderContextFlag::DisableSymbolClippingToExtent ); + + QPainter *painter = renderContext.painter(); painter->setPen( Qt::NoPen ); painter->setBrush( Qt::NoBrush ); - const double scale = context.renderContext().convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); + const double scale = renderContext.convertToPainterUnits( 1, Qgis::RenderUnit::Millimeters ); const QVector rings; //empty list - symbol()->startRender( context.renderContext() ); - symbol()->renderPolygon( calculatePolygon( scale ), &rings, nullptr, context.renderContext() ); - symbol()->stopRender( context.renderContext() ); + symbol()->startRender( renderContext ); + symbol()->renderPolygon( calculatePolygon( scale ), &rings, nullptr, renderContext ); + symbol()->stopRender( renderContext ); } QPolygonF QgsLayoutItemShape::calculatePolygon( double scale ) const diff --git a/src/core/qgis.h b/src/core/qgis.h index 14ea99d1f3d4..e5f17b03379a 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -2585,6 +2585,7 @@ class CORE_EXPORT Qgis SkipSymbolRendering = 0x40000, //!< Disable symbol rendering while still drawing labels if enabled \since QGIS 3.24 RecordProfile = 0x80000, //!< Enable run-time profiling while rendering \since QGIS 3.34 AlwaysUseGlobalMasks = 0x100000, //!< When applying clipping paths for selective masking, always use global ("entire map") paths, instead of calculating local clipping paths per rendered feature. This results in considerably more complex vector exports in all current Qt versions. This flag only applies to vector map exports. \since QGIS 3.38 + DisableSymbolClippingToExtent = 0x200000, //!< Force symbol clipping to map extent to be disabled in all situations. This will result in slower rendering, and should only be used in situations where the feature clipping is always undesirable. \since QGIS 3.40 }; //! Render context flags Q_DECLARE_FLAGS( RenderContextFlags, RenderContextFlag ) SIP_MONKEYPATCH_FLAGS_UNNEST( QgsRenderContext, Flags ) diff --git a/src/core/symbology/qgssymbol.cpp b/src/core/symbology/qgssymbol.cpp index 8f97b2134a2f..0c46efbddf47 100644 --- a/src/core/symbology/qgssymbol.cpp +++ b/src/core/symbology/qgssymbol.cpp @@ -1560,6 +1560,10 @@ void QgsSymbol::renderFeature( const QgsFeature &feature, QgsRenderContext &cont break; } } + if ( clippingEnabled && context.testFlag( Qgis::RenderContextFlag::DisableSymbolClippingToExtent ) ) + { + clippingEnabled = false; + } if ( clippingEnabled && context.testFlag( Qgis::RenderContextFlag::RenderMapTile ) ) { // If the "avoid artifacts between adjacent tiles" flag is set (RenderMapTile), then we'll force disable diff --git a/tests/src/python/test_qgslayoutpolygon.py b/tests/src/python/test_qgslayoutpolygon.py index bff8a02c5a94..115e41868a21 100644 --- a/tests/src/python/test_qgslayoutpolygon.py +++ b/tests/src/python/test_qgslayoutpolygon.py @@ -22,7 +22,11 @@ QgsLayoutItemRenderContext, QgsLayoutUtils, QgsProject, - QgsReadWriteContext + QgsReadWriteContext, + QgsLayoutItemMap, + QgsRectangle, + Qgis, + QgsGeometryGeneratorSymbolLayer ) import unittest from qgis.testing import start_app, QgisTestCase @@ -373,6 +377,51 @@ def testClipPath(self): p.end() self.assertEqual(len(spy), 5) + def test_generator(self): + project = QgsProject() + layout = QgsLayout(project) + layout.initializeDefaults() + + p = QPolygonF() + p.append(QPointF(0.0, 0.0)) + p.append(QPointF(100.0, 10.0)) + p.append(QPointF(200.0, 100.0)) + shape = QgsLayoutItemPolygon(p, layout) + layout.addLayoutItem(shape) + + map = QgsLayoutItemMap(layout) + map.attemptSetSceneRect(QRectF(0, 0, 10, 10)) + map.zoomToExtent(QgsRectangle(1, 1, 2, 2)) + layout.addLayoutItem(map) + + props = {} + props["color"] = "green" + props["style"] = "solid" + props["style_border"] = "solid" + props["color_border"] = "red" + props["width_border"] = "6.0" + props["joinstyle"] = "miter" + + sub_symbol = QgsFillSymbol.createSimple(props) + + line_symbol = QgsFillSymbol() + generator = QgsGeometryGeneratorSymbolLayer.create({ + 'geometryModifier': "geom_from_wkt('POLYGON((10 10,287 10,287 200,10 200,10 10))')", + 'SymbolType': 'Fill', + }) + generator.setUnits(Qgis.RenderUnit.Millimeters) + generator.setSubSymbol(sub_symbol) + + line_symbol.changeSymbolLayer(0, generator) + shape.setSymbol(line_symbol) + + self.assertTrue( + self.render_layout_check( + 'polygon_generator', + layout + ) + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgslayoutpolyline.py b/tests/src/python/test_qgslayoutpolyline.py index 8c165306a0cd..85350384e7e4 100644 --- a/tests/src/python/test_qgslayoutpolyline.py +++ b/tests/src/python/test_qgslayoutpolyline.py @@ -9,16 +9,20 @@ __date__ = '14/03/2016' __copyright__ = 'Copyright 2016, The QGIS Project' -from qgis.PyQt.QtCore import QPointF +from qgis.PyQt.QtCore import QPointF, QRectF from qgis.PyQt.QtGui import QPolygonF from qgis.PyQt.QtXml import QDomDocument from qgis.core import ( + Qgis, QgsLayout, QgsLayoutItemPolyline, + QgsLayoutItemMap, QgsLayoutItemRegistry, QgsLineSymbol, QgsProject, - QgsReadWriteContext + QgsReadWriteContext, + QgsGeometryGeneratorSymbolLayer, + QgsRectangle ) import unittest from qgis.testing import start_app, QgisTestCase @@ -384,6 +388,47 @@ def testVerticalLine(self): ) ) + def test_generator(self): + project = QgsProject() + layout = QgsLayout(project) + layout.initializeDefaults() + + p = QPolygonF() + p.append(QPointF(0.0, 0.0)) + p.append(QPointF(100.0, 100.0)) + shape = QgsLayoutItemPolyline(p, layout) + layout.addLayoutItem(shape) + + map = QgsLayoutItemMap(layout) + map.attemptSetSceneRect(QRectF(0, 0, 10, 10)) + map.zoomToExtent(QgsRectangle(1, 1, 2, 2)) + layout.addLayoutItem(map) + + props = {} + props["color"] = "0,0,0,255" + props["width"] = "10.0" + props["capstyle"] = "square" + + sub_symbol = QgsLineSymbol.createSimple(props) + + line_symbol = QgsLineSymbol() + generator = QgsGeometryGeneratorSymbolLayer.create({ + 'geometryModifier': "geom_from_wkt('POLYGON((10 10,287 10,287 200,10 200,10 10))')", + 'SymbolType': 'Line', + }) + generator.setUnits(Qgis.RenderUnit.Millimeters) + generator.setSubSymbol(sub_symbol) + + line_symbol.changeSymbolLayer(0, generator) + shape.setSymbol(line_symbol) + + self.assertTrue( + self.render_layout_check( + 'polyline_generator', + layout + ) + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgslayoutshape.py b/tests/src/python/test_qgslayoutshape.py index 7b76e7ef72a8..25a951149dc6 100644 --- a/tests/src/python/test_qgslayoutshape.py +++ b/tests/src/python/test_qgslayoutshape.py @@ -21,6 +21,10 @@ QgsProject, QgsReadWriteContext, QgsUnitTypes, + QgsLayoutItemMap, + Qgis, + QgsGeometryGeneratorSymbolLayer, + QgsRectangle ) import unittest from qgis.testing import start_app, QgisTestCase @@ -37,6 +41,10 @@ def setUpClass(cls): super(TestQgsLayoutShape, cls).setUpClass() cls.item_class = QgsLayoutItemShape + @classmethod + def control_path_prefix(cls): + return "layout_shape" + def testClipPath(self): pr = QgsProject() l = QgsLayout(pr) @@ -101,6 +109,49 @@ def testBoundingRectForStrokeSizeOnRestore(self): # bounding rect for item should include stroke self.assertEqual(shape2.boundingRect(), QRectF(-20.0, -20.0, 140.0, 240.0)) + def test_generator(self): + project = QgsProject() + layout = QgsLayout(project) + layout.initializeDefaults() + + shape = QgsLayoutItemShape(layout) + shape.setShapeType(QgsLayoutItemShape.Shape.Rectangle) + shape.attemptSetSceneRect(QRectF(0, 0, 100, 200)) + layout.addLayoutItem(shape) + + map = QgsLayoutItemMap(layout) + map.attemptSetSceneRect(QRectF(0, 0, 10, 10)) + map.zoomToExtent(QgsRectangle(1, 1, 2, 2)) + layout.addLayoutItem(map) + + props = {} + props["color"] = "green" + props["style"] = "solid" + props["style_border"] = "solid" + props["color_border"] = "red" + props["width_border"] = "6.0" + props["joinstyle"] = "miter" + + sub_symbol = QgsFillSymbol.createSimple(props) + + line_symbol = QgsFillSymbol() + generator = QgsGeometryGeneratorSymbolLayer.create({ + 'geometryModifier': "geom_from_wkt('POLYGON((10 10,287 10,287 200,10 200,10 10))')", + 'SymbolType': 'Fill', + }) + generator.setUnits(Qgis.RenderUnit.Millimeters) + generator.setSubSymbol(sub_symbol) + + line_symbol.changeSymbolLayer(0, generator) + shape.setSymbol(line_symbol) + + self.assertTrue( + self.render_layout_check( + 'layoutshape_generator', + layout + ) + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/testdata/control_images/composer_polygon/expected_polygon_generator/expected_polygon_generator.png b/tests/testdata/control_images/composer_polygon/expected_polygon_generator/expected_polygon_generator.png new file mode 100644 index 000000000000..5da6733807d2 Binary files /dev/null and b/tests/testdata/control_images/composer_polygon/expected_polygon_generator/expected_polygon_generator.png differ diff --git a/tests/testdata/control_images/composer_polyline/expected_polyline_generator/expected_polyline_generator.png b/tests/testdata/control_images/composer_polyline/expected_polyline_generator/expected_polyline_generator.png new file mode 100644 index 000000000000..d7fb669f02c6 Binary files /dev/null and b/tests/testdata/control_images/composer_polyline/expected_polyline_generator/expected_polyline_generator.png differ diff --git a/tests/testdata/control_images/layout_shape/expected_layoutshape_generator/expected_layoutshape_generator.png b/tests/testdata/control_images/layout_shape/expected_layoutshape_generator/expected_layoutshape_generator.png new file mode 100644 index 000000000000..5da6733807d2 Binary files /dev/null and b/tests/testdata/control_images/layout_shape/expected_layoutshape_generator/expected_layoutshape_generator.png differ