Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix clipped render when using geometry generator symbol in layout items #58926

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions python/PyQt6/core/auto_additions/qgis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


"""
# --
Expand Down
1 change: 1 addition & 0 deletions python/PyQt6/core/auto_generated/qgis.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,7 @@ The development version
SkipSymbolRendering,
RecordProfile,
AlwaysUseGlobalMasks,
DisableSymbolClippingToExtent,
};
typedef QFlags<Qgis::RenderContextFlag> RenderContextFlags;

Expand Down
7 changes: 7 additions & 0 deletions python/core/auto_additions/qgis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


"""
# --
Expand Down
1 change: 1 addition & 0 deletions python/core/auto_generated/qgis.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,7 @@ The development version
SkipSymbolRendering,
RecordProfile,
AlwaysUseGlobalMasks,
DisableSymbolClippingToExtent,
};
typedef QFlags<Qgis::RenderContextFlag> RenderContextFlags;

Expand Down
13 changes: 9 additions & 4 deletions src/core/layout/qgslayoutitempolygon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<QPolygonF> 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 )
Expand Down
21 changes: 13 additions & 8 deletions src/core/layout/qgslayoutitempolyline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand Down
15 changes: 10 additions & 5 deletions src/core/layout/qgslayoutitemshape.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<QPolygonF> 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
Expand Down
1 change: 1 addition & 0 deletions src/core/qgis.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand Down
4 changes: 4 additions & 0 deletions src/core/symbology/qgssymbol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,10 @@ void QgsSymbol::renderFeature( const QgsFeature &feature, QgsRenderContext &cont
break;
}
}
if ( clippingEnabled && context.testFlag( Qgis::RenderContextFlag::DisableSymbolClippingToExtent ) )
{
clippingEnabled = false;
}
Comment on lines +1563 to +1566
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ( clippingEnabled && context.testFlag( Qgis::RenderContextFlag::DisableSymbolClippingToExtent ) )
{
clippingEnabled = false;
}
clippingEnabled &= !context.testFlag( Qgis::RenderContextFlag::DisableSymbolClippingToExtent )

Cosmetic proposal. less code, better readability. We could also group all other conditions related to this boolean, but that's not mandatory.

if ( clippingEnabled && context.testFlag( Qgis::RenderContextFlag::RenderMapTile ) )
{
// If the "avoid artifacts between adjacent tiles" flag is set (RenderMapTile), then we'll force disable
Expand Down
51 changes: 50 additions & 1 deletion tests/src/python/test_qgslayoutpolygon.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
QgsLayoutItemRenderContext,
QgsLayoutUtils,
QgsProject,
QgsReadWriteContext
QgsReadWriteContext,
QgsLayoutItemMap,
QgsRectangle,
Qgis,
QgsGeometryGeneratorSymbolLayer
)
import unittest
from qgis.testing import start_app, QgisTestCase
Expand Down Expand Up @@ -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()
49 changes: 47 additions & 2 deletions tests/src/python/test_qgslayoutpolyline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
51 changes: 51 additions & 0 deletions tests/src/python/test_qgslayoutshape.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
QgsProject,
QgsReadWriteContext,
QgsUnitTypes,
QgsLayoutItemMap,
Qgis,
QgsGeometryGeneratorSymbolLayer,
QgsRectangle
)
import unittest
from qgis.testing import start_app, QgisTestCase
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading