Skip to content
Permalink
Browse files
[feature][symbolgy] Expose choice of clipping behaviour for line
pattern fill

This allows users to control how lines in the fill should be
clipped to the polygon shape. Options are:

- Clip During Render Only: existing behaviour, lines are created
covering the whole bounding box of the feature and then clipped
while drawing. Line extremities (beginning and end) will not be
visible
- Clip Lines Before Render: lines are clipped to the exact
shape of the polygon prior to rendering. Line extremities (including
cap styles, start/end marker line objects, etc) will be visible,
and may sometimes extend outside of the polygon (depending
on the line symbol settings)
- No Clipping: no clipping at all is done - line will cover the
whole bounding box of the feature

Sponsored by North Road, thanks to SLYR
  • Loading branch information
nyalldawson committed Oct 26, 2021
1 parent ef3983c commit b06e136a578a644908af2fcc691fcee49f5f276a
@@ -1098,3 +1098,10 @@
Qgis.MarkerClipMode.__doc__ = 'Marker clipping modes.\n\n.. versionadded:: 3.24\n\n' + '* ``NoClipping``: ' + Qgis.MarkerClipMode.NoClipping.__doc__ + '\n' + '* ``Shape``: ' + Qgis.MarkerClipMode.Shape.__doc__ + '\n' + '* ``CentroidWithin``: ' + Qgis.MarkerClipMode.CentroidWithin.__doc__ + '\n' + '* ``CompletelyWithin``: ' + Qgis.MarkerClipMode.CompletelyWithin.__doc__
# --
Qgis.MarkerClipMode.baseClass = Qgis
# monkey patching scoped based enum
Qgis.LineClipMode.ClipPainterOnly.__doc__ = "Applying clipping on the painter only (i.e. line endpoints will coincide with polygon bounding box, but will not be part of the visible portion of the line)"
Qgis.LineClipMode.ClipToIntersection.__doc__ = "Clip lines to intersection with polygon shape (slower) (i.e. line endpoints will coincide with polygon exterior)"
Qgis.LineClipMode.NoClipping.__doc__ = "Lines are not clipped, will extend to shape's bounding box."
Qgis.LineClipMode.__doc__ = 'Line clipping modes.\n\n.. versionadded:: 3.24\n\n' + '* ``ClipPainterOnly``: ' + Qgis.LineClipMode.ClipPainterOnly.__doc__ + '\n' + '* ``ClipToIntersection``: ' + Qgis.LineClipMode.ClipToIntersection.__doc__ + '\n' + '* ``NoClipping``: ' + Qgis.LineClipMode.NoClipping.__doc__
# --
Qgis.LineClipMode.baseClass = Qgis
@@ -700,6 +700,13 @@ The development version
CompletelyWithin,
};

enum class LineClipMode
{
ClipPainterOnly,
ClipToIntersection,
NoClipping,
};

static const double DEFAULT_SEARCH_RADIUS_MM;

static const float DEFAULT_MAPTOPIXEL_THRESHOLD;
@@ -1747,6 +1747,24 @@ Returns the map unit scale for the pattern's line offset.
.. seealso:: :py:func:`offset`

.. seealso:: :py:func:`offsetUnit`
%End

Qgis::LineClipMode clipMode() const;
%Docstring
Returns the line clipping mode, which defines how lines are clipped at the edges of shapes.

.. seealso:: :py:func:`setClipMode`

.. versionadded:: 3.24
%End

void setClipMode( Qgis::LineClipMode mode );
%Docstring
Sets the line clipping ``mode``, which defines how lines are clipped at the edges of shapes.

.. seealso:: :py:func:`clipMode`

.. versionadded:: 3.24
%End

virtual void setOutputUnit( QgsUnitTypes::RenderUnit unit );
@@ -159,6 +159,7 @@ class QgsSymbolLayer
PropertyMarkerClipping,
PropertyRandomOffsetX,
PropertyRandomOffsetY,
PropertyLineClipping,
};

static const QgsPropertiesDefinition &propertyDefinitions();
@@ -115,6 +115,29 @@ Encodes a marker clip ``mode`` to a string.

.. seealso:: :py:func:`decodeMarkerClipMode`

.. versionadded:: 3.24
%End

static Qgis::LineClipMode decodeLineClipMode( const QString &string, bool *ok /Out/ = 0 );
%Docstring
Decodes a ``string`` representing a line clip mode.

:param string: string to decode

:return: - decoded line clip mode
- ok: will be set to ``True`` if ``string`` was successfully decoded

.. seealso:: :py:func:`encodeLineClipMode`

.. versionadded:: 3.24
%End

static QString encodeLineClipMode( Qgis::LineClipMode mode );
%Docstring
Encodes a line clip ``mode`` to a string.

.. seealso:: :py:func:`decodeLineClipMode`

.. versionadded:: 3.24
%End

@@ -1140,6 +1140,19 @@ class CORE_EXPORT Qgis
};
Q_ENUM( MarkerClipMode )

/**
* Line clipping modes.
*
* \since QGIS 3.24
*/
enum class LineClipMode : int
{
ClipPainterOnly, //!< Applying clipping on the painter only (i.e. line endpoints will coincide with polygon bounding box, but will not be part of the visible portion of the line)
ClipToIntersection, //!< Clip lines to intersection with polygon shape (slower) (i.e. line endpoints will coincide with polygon exterior)
NoClipping, //!< Lines are not clipped, will extend to shape's bounding box.
};
Q_ENUM( LineClipMode )

/**
* Identify search radius in mm
* \since QGIS 2.3
@@ -2717,6 +2717,10 @@ QgsSymbolLayer *QgsLinePatternFillSymbolLayer::create( const QVariantMap &proper
{
patternLayer->setCoordinateReference( QgsSymbolLayerUtils::decodeCoordinateReference( properties[QStringLiteral( "coordinate_reference" )].toString() ) );
}
if ( properties.contains( QStringLiteral( "clip_mode" ) ) )
{
patternLayer->setClipMode( QgsSymbolLayerUtils::decodeLineClipMode( properties.value( QStringLiteral( "clip_mode" ) ).toString() ) );
}

patternLayer->restoreOldDataDefinedProperties( properties );

@@ -3021,7 +3025,9 @@ void QgsLinePatternFillSymbolLayer::startRender( QgsSymbolRenderContext &context
// if we are using a vector based output, we need to render points as vectors
// (OR if the line has data defined symbology, in which case we need to evaluate this line-by-line)
mRenderUsingLines = context.renderContext().forceVectorOutput()
|| mFillLineSymbol->hasDataDefinedProperties();
|| mFillLineSymbol->hasDataDefinedProperties()
|| mClipMode != Qgis::LineClipMode::ClipPainterOnly
|| mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyLineClipping );

if ( mRenderUsingLines )
{
@@ -3102,16 +3108,58 @@ void QgsLinePatternFillSymbolLayer::renderPolygon( const QPolygonF &points, cons

p->save();

QPainterPath path;
path.addPolygon( points );
if ( rings )
Qgis::LineClipMode clipMode = mClipMode;
if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyLineClipping ) )
{
for ( const QPolygonF &ring : *rings )
context.setOriginalValueVariable( QgsSymbolLayerUtils::encodeLineClipMode( clipMode ) );
bool ok = false;
const QString valueString = mDataDefinedProperties.valueAsString( QgsSymbolLayer::PropertyLineClipping, context.renderContext().expressionContext(), QString(), &ok );
if ( ok )
{
path.addPolygon( ring );
Qgis::LineClipMode decodedMode = QgsSymbolLayerUtils::decodeLineClipMode( valueString, &ok );
if ( ok )
clipMode = decodedMode;
}
}

std::unique_ptr< QgsPolygon > shapePolygon;
std::unique_ptr< QgsGeometryEngine > shapeEngine;
switch ( clipMode )
{
case Qgis::LineClipMode::NoClipping:
break;

case Qgis::LineClipMode::ClipToIntersection:
{
shapePolygon = std::make_unique< QgsPolygon >();
shapePolygon->setExteriorRing( QgsLineString::fromQPolygonF( points ) );
if ( rings )
{
for ( const QPolygonF &ring : *rings )
{
shapePolygon->addInteriorRing( QgsLineString::fromQPolygonF( ring ) );
}
}
shapeEngine.reset( QgsGeometry::createGeometryEngine( shapePolygon.get() ) );
shapeEngine->prepareGeometry();
break;
}

case Qgis::LineClipMode::ClipPainterOnly:
{
QPainterPath path;
path.addPolygon( points );
if ( rings )
{
for ( const QPolygonF &ring : *rings )
{
path.addPolygon( ring );
}
}
p->setClipPath( path, Qt::IntersectClip );
break;
}
}
p->setClipPath( path, Qt::IntersectClip );

const bool applyBrushTransform = applyBrushTransformFromContext( &context );
const QRectF boundingRect = points.boundingRect();
@@ -3176,13 +3224,22 @@ void QgsLinePatternFillSymbolLayer::renderPolygon( const QPolygonF &points, cons
invertedRotateTransform.map( left, currentY - outputPixelOffset, &x1, &y1 );
invertedRotateTransform.map( right, currentY - outputPixelOffset, &x2, &y2 );

// todo -- if we do proper intersects clipping, we may end up with multiline string here, so we'd need to wrap the
// rendering of each part with in
// mFillLineSymbol->startFeatureRender();
// ...
// mFillLineSymbol->stopFeatureRender();

mFillLineSymbol->renderPolyline( QPolygonF() << QPointF( x1, y1 ) << QPointF( x2, y2 ), context.feature(), context.renderContext() );
if ( shapeEngine )
{
QgsLineString ls( QgsPoint( x1, y1 ), QgsPoint( x2, y2 ) );
std::unique_ptr< QgsAbstractGeometry > intersection( shapeEngine->intersection( &ls ) );
for ( auto it = intersection->const_parts_begin(); it != intersection->const_parts_end(); ++it )
{
if ( const QgsLineString *ls = qgsgeometry_cast< const QgsLineString * >( *it ) )
{
mFillLineSymbol->renderPolyline( ls->asQPolygonF(), context.feature(), context.renderContext() );
}
}
}
else
{
mFillLineSymbol->renderPolyline( QPolygonF() << QPointF( x1, y1 ) << QPointF( x2, y2 ), context.feature(), context.renderContext() );
}
}

p->restore();
@@ -3206,6 +3263,7 @@ QVariantMap QgsLinePatternFillSymbolLayer::properties() const
map.insert( QStringLiteral( "offset_map_unit_scale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mOffsetMapUnitScale ) );
map.insert( QStringLiteral( "outline_width_unit" ), QgsUnitTypes::encodeUnit( mStrokeWidthUnit ) );
map.insert( QStringLiteral( "outline_width_map_unit_scale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mStrokeWidthMapUnitScale ) );
map.insert( QStringLiteral( "clip_mode" ), QgsSymbolLayerUtils::encodeLineClipMode( mClipMode ) );
return map;
}

@@ -1595,6 +1595,22 @@ class CORE_EXPORT QgsLinePatternFillSymbolLayer: public QgsImageFillSymbolLayer
*/
const QgsMapUnitScale &offsetMapUnitScale() const { return mOffsetMapUnitScale; }

/**
* Returns the line clipping mode, which defines how lines are clipped at the edges of shapes.
*
* \see setClipMode()
* \since QGIS 3.24
*/
Qgis::LineClipMode clipMode() const { return mClipMode; }

/**
* Sets the line clipping \a mode, which defines how lines are clipped at the edges of shapes.
*
* \see clipMode()
* \since QGIS 3.24
*/
void setClipMode( Qgis::LineClipMode mode ) { mClipMode = mode; }

void setOutputUnit( QgsUnitTypes::RenderUnit unit ) override;
QgsUnitTypes::RenderUnit outputUnit() const override;
bool usesMapUnits() const override;
@@ -1638,6 +1654,8 @@ class CORE_EXPORT QgsLinePatternFillSymbolLayer: public QgsImageFillSymbolLayer

//! Fill line
std::unique_ptr< QgsLineSymbol > mFillLineSymbol;

Qgis::LineClipMode mClipMode = Qgis::LineClipMode::ClipPainterOnly;
};

/**
@@ -119,6 +119,7 @@ void QgsSymbolLayer::initPropertyDefinitions()
{ QgsSymbolLayer::PropertyMarkerClipping, QgsPropertyDefinition( "markerClipping", QgsPropertyDefinition::DataTypeString, QObject::tr( "Marker clipping mode" ), QObject::tr( "string " ) + QLatin1String( "[<b>no</b>|<b>shape</b>|<b>centroid_within</b>|<b>completely_within</b>]" ), origin )},
{ QgsSymbolLayer::PropertyRandomOffsetX, QgsPropertyDefinition( "randomOffsetX", QObject::tr( "Horizontal random offset" ), QgsPropertyDefinition::Double, origin )},
{ QgsSymbolLayer::PropertyRandomOffsetY, QgsPropertyDefinition( "randomOffsetY", QObject::tr( "Vertical random offset" ), QgsPropertyDefinition::Double, origin )},
{ QgsSymbolLayer::PropertyLineClipping, QgsPropertyDefinition( "lineClipping", QgsPropertyDefinition::DataTypeString, QObject::tr( "Line clipping mode" ), QObject::tr( "string " ) + QLatin1String( "[<b>no</b>|<b>during_render</b>|<b>before_render</b>]" ), origin )},
};
}

@@ -203,6 +203,7 @@ class CORE_EXPORT QgsSymbolLayer
PropertyMarkerClipping, //!< Marker clipping mode (since QGIS 3.24)
PropertyRandomOffsetX, //!< Random offset X (since QGIS 3.24)
PropertyRandomOffsetY, //!< Random offset Y (since QGIS 3.24)
PropertyLineClipping, //!< Line clipping mode (since QGIS 3.24)
};

/**
@@ -501,6 +501,38 @@ QString QgsSymbolLayerUtils::encodeMarkerClipMode( Qgis::MarkerClipMode mode )
return QString(); // no warnings
}

Qgis::LineClipMode QgsSymbolLayerUtils::decodeLineClipMode( const QString &string, bool *ok )
{
const QString compareString = string.trimmed();
if ( ok )
*ok = true;

if ( compareString.compare( QLatin1String( "no" ), Qt::CaseInsensitive ) == 0 )
return Qgis::LineClipMode::NoClipping;
else if ( compareString.compare( QLatin1String( "during_render" ), Qt::CaseInsensitive ) == 0 )
return Qgis::LineClipMode::ClipPainterOnly;
else if ( compareString.compare( QLatin1String( "before_render" ), Qt::CaseInsensitive ) == 0 )
return Qgis::LineClipMode::ClipToIntersection;

if ( ok )
*ok = false;
return Qgis::LineClipMode::ClipPainterOnly;
}

QString QgsSymbolLayerUtils::encodeLineClipMode( Qgis::LineClipMode mode )
{
switch ( mode )
{
case Qgis::LineClipMode::NoClipping:
return QStringLiteral( "no" );
case Qgis::LineClipMode::ClipPainterOnly:
return QStringLiteral( "during_render" );
case Qgis::LineClipMode::ClipToIntersection:
return QStringLiteral( "before_render" );
}
return QString(); // no warnings
}

QString QgsSymbolLayerUtils::encodePoint( QPointF point )
{
return QStringLiteral( "%1,%2" ).arg( qgsDoubleToString( point.x() ), qgsDoubleToString( point.y() ) );
@@ -144,6 +144,26 @@ class CORE_EXPORT QgsSymbolLayerUtils
*/
static QString encodeMarkerClipMode( Qgis::MarkerClipMode mode );

/**
* Decodes a \a string representing a line clip mode.
*
* \param string string to decode
* \param ok will be set to TRUE if \a string was successfully decoded
* \returns decoded line clip mode
*
* \see encodeLineClipMode()
* \since QGIS 3.24
*/
static Qgis::LineClipMode decodeLineClipMode( const QString &string, bool *ok SIP_OUT = nullptr );

/**
* Encodes a line clip \a mode to a string.
*
* \see decodeLineClipMode()
* \since QGIS 3.24
*/
static QString encodeLineClipMode( Qgis::LineClipMode mode );

/**
* Encodes a QPointF to a string.
* \see decodePoint()
@@ -3095,6 +3095,19 @@ QgsLinePatternFillSymbolLayerWidget::QgsLinePatternFillSymbolLayerWidget( QgsVec
emit changed();
}
} );

mClipModeComboBox->addItem( tr( "Clip During Render Only" ), static_cast< int >( Qgis::LineClipMode::ClipPainterOnly ) );
mClipModeComboBox->addItem( tr( "Clip Lines Before Render" ), static_cast< int >( Qgis::LineClipMode::ClipToIntersection ) );
mClipModeComboBox->addItem( tr( "No Clipping" ), static_cast< int >( Qgis::LineClipMode::NoClipping ) );
connect( mClipModeComboBox, qOverload< int >( &QComboBox::currentIndexChanged ), this, [ = ]
{
if ( mLayer )
{
mLayer->setClipMode( static_cast< Qgis::LineClipMode >( mClipModeComboBox->currentData().toInt() ) );
emit changed();
}
} );

}

void QgsLinePatternFillSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer )
@@ -3123,11 +3136,14 @@ void QgsLinePatternFillSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer
mOffsetUnitWidget->blockSignals( false );

whileBlocking( mCoordinateReferenceComboBox )->setCurrentIndex( mCoordinateReferenceComboBox->findData( static_cast< int >( mLayer->coordinateReference() ) ) );

whileBlocking( mClipModeComboBox )->setCurrentIndex( mClipModeComboBox->findData( static_cast< int >( mLayer->clipMode() ) ) );
}

registerDataDefinedButton( mAngleDDBtn, QgsSymbolLayer::PropertyLineAngle );
registerDataDefinedButton( mDistanceDDBtn, QgsSymbolLayer::PropertyLineDistance );
registerDataDefinedButton( mCoordinateReferenceDDBtn, QgsSymbolLayer::PropertyCoordinateMode );
registerDataDefinedButton( mClippingDDBtn, QgsSymbolLayer::PropertyLineClipping );
}

QgsSymbolLayer *QgsLinePatternFillSymbolLayerWidget::symbolLayer()

0 comments on commit b06e136

Please sign in to comment.