Skip to content

Commit

Permalink
[feature][symbolgy] Expose choice of clipping behaviour for line
Browse files Browse the repository at this point in the history
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 b06e136
Show file tree
Hide file tree
Showing 20 changed files with 491 additions and 74 deletions.
7 changes: 7 additions & 0 deletions python/core/auto_additions/qgis.py
Expand Up @@ -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
7 changes: 7 additions & 0 deletions python/core/auto_generated/qgis.sip.in
Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions python/core/auto_generated/symbology/qgsfillsymbollayer.sip.in
Expand Up @@ -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 );
Expand Down
1 change: 1 addition & 0 deletions python/core/auto_generated/symbology/qgssymbollayer.sip.in
Expand Up @@ -159,6 +159,7 @@ class QgsSymbolLayer
PropertyMarkerClipping,
PropertyRandomOffsetX,
PropertyRandomOffsetY,
PropertyLineClipping,
};

static const QgsPropertiesDefinition &propertyDefinitions();
Expand Down
23 changes: 23 additions & 0 deletions python/core/auto_generated/symbology/qgssymbollayerutils.sip.in
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions src/core/qgis.h
Expand Up @@ -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
Expand Down
86 changes: 72 additions & 14 deletions src/core/symbology/qgsfillsymbollayer.cpp
Expand Up @@ -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 );

Expand Down Expand Up @@ -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 )
{
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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;
}

Expand Down
18 changes: 18 additions & 0 deletions src/core/symbology/qgsfillsymbollayer.h
Expand Up @@ -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;
Expand Down Expand Up @@ -1638,6 +1654,8 @@ class CORE_EXPORT QgsLinePatternFillSymbolLayer: public QgsImageFillSymbolLayer

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

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

/**
Expand Down
1 change: 1 addition & 0 deletions src/core/symbology/qgssymbollayer.cpp
Expand Up @@ -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 )},
};
}

Expand Down
1 change: 1 addition & 0 deletions src/core/symbology/qgssymbollayer.h
Expand Up @@ -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)
};

/**
Expand Down
32 changes: 32 additions & 0 deletions src/core/symbology/qgssymbollayerutils.cpp
Expand Up @@ -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() ) );
Expand Down
20 changes: 20 additions & 0 deletions src/core/symbology/qgssymbollayerutils.h
Expand Up @@ -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()
Expand Down
16 changes: 16 additions & 0 deletions src/gui/symbology/qgssymbollayerwidget.cpp
Expand Up @@ -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 )
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit b06e136

Please sign in to comment.