From 35784b2263c9228adbe14c5532cc1fd41b59f650 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 25 May 2024 16:36:01 +0700 Subject: [PATCH] [advanced digitizing] Implement visual construction guides --- .../core/auto_generated/qgscadutils.sip.in | 1 - .../qgsadvanceddigitizingdockwidget.sip.in | 31 ++- python/core/auto_generated/qgscadutils.sip.in | 1 - .../qgsadvanceddigitizingdockwidget.sip.in | 31 ++- src/core/qgscadutils.cpp | 4 +- src/core/qgscadutils.h | 1 - src/gui/qgsadvanceddigitizingcanvasitem.cpp | 27 ++- src/gui/qgsadvanceddigitizingcanvasitem.h | 1 + src/gui/qgsadvanceddigitizingdockwidget.cpp | 194 ++++++++++++++++++ src/gui/qgsadvanceddigitizingdockwidget.h | 62 +++++- tests/src/app/testqgsadvanceddigitizing.cpp | 52 +++++ 11 files changed, 388 insertions(+), 17 deletions(-) diff --git a/python/PyQt6/core/auto_generated/qgscadutils.sip.in b/python/PyQt6/core/auto_generated/qgscadutils.sip.in index 6fe83fb37918..3c2c4438730b 100644 --- a/python/PyQt6/core/auto_generated/qgscadutils.sip.in +++ b/python/PyQt6/core/auto_generated/qgscadutils.sip.in @@ -132,7 +132,6 @@ Returns the recent CAD point at the specified ``index`` (in map coordinates). .. versionadded:: 3.22 %End - %Property( name = cadPointList, get = _cadPointList, set = _setCadPointList ) void _setCadPointList( const QList< QgsPointXY > &list ); QList< QgsPointXY > _cadPointList() const; diff --git a/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in b/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in index d1fcac710503..874873f94a2c 100644 --- a/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in @@ -277,7 +277,36 @@ Sets whether M is enabled bool constructionMode() const; %Docstring -construction mode is used to draw intermediate points. These points won't be given any further (i.e. to the map tools) +Returns whether the construction mode is activated. The construction mode is used to draw intermediate +points that will not be part of a geometry being digitized. +%End + + QgsVectorLayer *constructionGuidesLayer() const; +%Docstring +Returns the vector layer within which construction guides are stored. + +.. versionadded:: 3.40 +%End + + bool showConstructionGuides() const; +%Docstring +Returns whether the construction guides are visible. + +.. versionadded:: 3.40 +%End + + bool snapToConstructionGuides() const; +%Docstring +Returns whether points should snap to construction guides. + +.. versionadded:: 3.40 +%End + + bool recordConstructionGuides() const; +%Docstring +Returns whether construction guides are being recorded. + +.. versionadded:: 3.40 %End Qgis::BetweenLineConstraint betweenLineConstraint() const; diff --git a/python/core/auto_generated/qgscadutils.sip.in b/python/core/auto_generated/qgscadutils.sip.in index 6fe83fb37918..3c2c4438730b 100644 --- a/python/core/auto_generated/qgscadutils.sip.in +++ b/python/core/auto_generated/qgscadutils.sip.in @@ -132,7 +132,6 @@ Returns the recent CAD point at the specified ``index`` (in map coordinates). .. versionadded:: 3.22 %End - %Property( name = cadPointList, get = _cadPointList, set = _setCadPointList ) void _setCadPointList( const QList< QgsPointXY > &list ); QList< QgsPointXY > _cadPointList() const; diff --git a/python/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in b/python/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in index 7b1058258bb9..bb6476e19a17 100644 --- a/python/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in +++ b/python/gui/auto_generated/qgsadvanceddigitizingdockwidget.sip.in @@ -277,7 +277,36 @@ Sets whether M is enabled bool constructionMode() const; %Docstring -construction mode is used to draw intermediate points. These points won't be given any further (i.e. to the map tools) +Returns whether the construction mode is activated. The construction mode is used to draw intermediate +points that will not be part of a geometry being digitized. +%End + + QgsVectorLayer *constructionGuidesLayer() const; +%Docstring +Returns the vector layer within which construction guides are stored. + +.. versionadded:: 3.40 +%End + + bool showConstructionGuides() const; +%Docstring +Returns whether the construction guides are visible. + +.. versionadded:: 3.40 +%End + + bool snapToConstructionGuides() const; +%Docstring +Returns whether points should snap to construction guides. + +.. versionadded:: 3.40 +%End + + bool recordConstructionGuides() const; +%Docstring +Returns whether construction guides are being recorded. + +.. versionadded:: 3.40 %End Qgis::BetweenLineConstraint betweenLineConstraint() const; diff --git a/src/core/qgscadutils.cpp b/src/core/qgscadutils.cpp index 84d5875cfb17..943d8c31b9de 100644 --- a/src/core/qgscadutils.cpp +++ b/src/core/qgscadutils.cpp @@ -48,8 +48,8 @@ QgsCadUtils::AlignMapPointOutput QgsCadUtils::alignMapPoint( const QgsPointXY &o res.softLockX = std::numeric_limits::quiet_NaN(); res.softLockY = std::numeric_limits::quiet_NaN(); - // try to snap to anything - const QgsPointLocator::Match snapMatch = ctx.snappingUtils->snapToMap( originalMapPoint, nullptr, true ); + // try to snap to project layer(s) as well as visible construction guides + QgsPointLocator::Match snapMatch = ctx.snappingUtils->snapToMap( originalMapPoint, nullptr, true ); res.snapMatch = snapMatch; QgsPointXY point = snapMatch.isValid() ? snapMatch.point() : originalMapPoint; QgsPointXY edgePt0, edgePt1; diff --git a/src/core/qgscadutils.h b/src/core/qgscadutils.h index 1d9e59a9575a..f4783839369a 100644 --- a/src/core/qgscadutils.h +++ b/src/core/qgscadutils.h @@ -195,7 +195,6 @@ class CORE_EXPORT QgsCadUtils */ QQueue< QgsPointLocator::Match > lockedSnapVertices() const { return mLockedSnapVertices; } SIP_SKIP; - #ifdef SIP_RUN SIP_PROPERTY( name = cadPointList, get = _cadPointList, set = _setCadPointList ) #endif diff --git a/src/gui/qgsadvanceddigitizingcanvasitem.cpp b/src/gui/qgsadvanceddigitizingcanvasitem.cpp index fd827db13836..fcc9e0dc5c85 100644 --- a/src/gui/qgsadvanceddigitizingcanvasitem.cpp +++ b/src/gui/qgsadvanceddigitizingcanvasitem.cpp @@ -28,6 +28,7 @@ QgsAdvancedDigitizingCanvasItem::QgsAdvancedDigitizingCanvasItem( QgsMapCanvas * , mSnapPen( QPen( QColor( 127, 0, 0, 150 ), 1 ) ) , mSnapLinePen( QPen( QColor( 127, 0, 0, 150 ), 1, Qt::DashLine ) ) , mCursorPen( QPen( QColor( 127, 127, 127, 255 ), 1 ) ) + , mConstructionGuidesPen( QPen( QColor( 20, 210, 150 ), 1, Qt::DashLine ) ) , mAdvancedDigitizingDockWidget( cadDockWidget ) { } @@ -37,6 +38,27 @@ void QgsAdvancedDigitizingCanvasItem::paint( QPainter *painter ) if ( !mAdvancedDigitizingDockWidget->cadEnabled() ) return; + painter->setRenderHint( QPainter::Antialiasing ); + painter->setCompositionMode( QPainter::CompositionMode_Difference ); + + // Draw construction guides + if ( mAdvancedDigitizingDockWidget->showConstructionGuides() ) + { + if ( QgsVectorLayer *constructionGuidesLayer = mAdvancedDigitizingDockWidget->constructionGuidesLayer() ) + { + QgsFeatureIterator it = constructionGuidesLayer->getFeatures( QgsFeatureRequest().setNoAttributes().setFilterRect( mMapCanvas->mapSettings().visibleExtent() ) ); + QgsFeature feature; + painter->setPen( mConstructionGuidesPen ); + while ( it.nextFeature( feature ) ) + { + QgsGeometry geom = feature.geometry(); + geom.mapToPixel( *mMapCanvas->getCoordinateTransform() ); + const QPolygonF polygon = geom.asQPolygonF(); + painter->drawPolyline( polygon ); + } + } + } + // Use visible polygon rather than extent to properly handle rotated maps QPolygonF mapPoly = mMapCanvas->mapSettings().visiblePolygon(); const double canvasWidth = QLineF( mapPoly[0], mapPoly[1] ).length(); @@ -83,9 +105,6 @@ void QgsAdvancedDigitizingCanvasItem::paint( QPainter *painter ) snapSegmentPix2 = toCanvasCoordinates( snappedSegment[1] ); } - painter->setRenderHint( QPainter::Antialiasing ); - painter->setCompositionMode( QPainter::CompositionMode_Difference ); - // Draw point snap if ( curPointExist && snappedToVertex ) { @@ -226,7 +245,7 @@ void QgsAdvancedDigitizingCanvasItem::paint( QPainter *painter ) } } - // Draw constr + // Draw constraints if ( mAdvancedDigitizingDockWidget->betweenLineConstraint() == Qgis::BetweenLineConstraint::NoConstraint ) { if ( curPointExist && previousPointExist ) diff --git a/src/gui/qgsadvanceddigitizingcanvasitem.h b/src/gui/qgsadvanceddigitizingcanvasitem.h index 9678fffb793c..a6c90dfe025b 100644 --- a/src/gui/qgsadvanceddigitizingcanvasitem.h +++ b/src/gui/qgsadvanceddigitizingcanvasitem.h @@ -60,6 +60,7 @@ class GUI_EXPORT QgsAdvancedDigitizingCanvasItem : public QgsMapCanvasItem QPen mSnapPen; QPen mSnapLinePen; QPen mCursorPen; + QPen mConstructionGuidesPen; QgsAdvancedDigitizingDockWidget *mAdvancedDigitizingDockWidget = nullptr; }; diff --git a/src/gui/qgsadvanceddigitizingdockwidget.cpp b/src/gui/qgsadvanceddigitizingdockwidget.cpp index 741448e2fdf6..05db8214b083 100644 --- a/src/gui/qgsadvanceddigitizingdockwidget.cpp +++ b/src/gui/qgsadvanceddigitizingdockwidget.cpp @@ -43,6 +43,9 @@ const QgsSettingsEntryBool *QgsAdvancedDigitizingDockWidget::settingsCadSnappingPriorityPrioritizeFeature = new QgsSettingsEntryBool( QStringLiteral( "cad-snapping-prioritize-feature" ), QgsSettingsTree::sTreeDigitizing, false, tr( "Determines if snapping to features has priority over snapping to common angles." ) ) ; +const QgsSettingsEntryBool *QgsAdvancedDigitizingDockWidget::settingsCadRecordConstructionGuides = new QgsSettingsEntryBool( QStringLiteral( "cad-record-construction-guides" ), QgsSettingsTree::sTreeDigitizing, false, tr( "Determines if construction guides are being recorded." ) ) ; +const QgsSettingsEntryBool *QgsAdvancedDigitizingDockWidget::settingsCadShowConstructionGuides = new QgsSettingsEntryBool( QStringLiteral( "cad-show-construction-guides" ), QgsSettingsTree::sTreeDigitizing, true, tr( "Determines whether construction guides are shown." ) ) ; +const QgsSettingsEntryBool *QgsAdvancedDigitizingDockWidget::settingsCadSnapToConstructionGuides = new QgsSettingsEntryBool( QStringLiteral( "cad-snap-to-construction-guides" ), QgsSettingsTree::sTreeDigitizing, false, tr( "Determines if points will snap to construction guides." ) ) ; QgsAdvancedDigitizingDockWidget::QgsAdvancedDigitizingDockWidget( QgsMapCanvas *canvas, QWidget *parent ) @@ -194,6 +197,46 @@ QgsAdvancedDigitizingDockWidget::QgsAdvancedDigitizingDockWidget( QgsMapCanvas * mCommonAngleActions.insert( it->first, action ); } + // Construction modes + QMenu *constructionSettingsMenu = new QMenu( this ); + + mRecordConstructionGuides = new QAction( tr( "Record Construction Guides" ), constructionSettingsMenu ); + mRecordConstructionGuides->setCheckable( true ); + mRecordConstructionGuides->setChecked( settingsCadRecordConstructionGuides->value() ); + constructionSettingsMenu->addAction( mRecordConstructionGuides ); + connect( mRecordConstructionGuides, &QAction::triggered, this, [ = ]() { settingsCadRecordConstructionGuides->setValue( mRecordConstructionGuides->isChecked() ); } ); + + mShowConstructionGuides = new QAction( tr( "Show Construction Guides" ), constructionSettingsMenu ); + mShowConstructionGuides->setCheckable( true ); + mShowConstructionGuides->setChecked( settingsCadShowConstructionGuides->value() ); + constructionSettingsMenu->addAction( mShowConstructionGuides ); + connect( mShowConstructionGuides, &QAction::triggered, this, [ = ]() + { + settingsCadShowConstructionGuides->setValue( mShowConstructionGuides->isChecked() ); + updateCadPaintItem(); + } ); + + mSnapToConstructionGuides = new QAction( tr( "Snap to Visible Construction Guides" ), constructionSettingsMenu ); + mSnapToConstructionGuides->setCheckable( true ); + mSnapToConstructionGuides->setChecked( settingsCadSnapToConstructionGuides->value() ); + constructionSettingsMenu->addAction( mSnapToConstructionGuides ); + connect( mSnapToConstructionGuides, &QAction::triggered, this, [ = ]() { settingsCadSnapToConstructionGuides->setValue( mSnapToConstructionGuides->isChecked() ); } ); + + constructionSettingsMenu->addSeparator(); + + mClearConstructionGuides = new QAction( tr( "Clear Construction Guides" ), constructionSettingsMenu ); + constructionSettingsMenu->addAction( mClearConstructionGuides ); + connect( mClearConstructionGuides, &QAction::triggered, this, [ = ]() + { + resetConstructionGuides(); + updateCadPaintItem(); + } ); + + QToolButton *constructionModeToolButton = qobject_cast< QToolButton *>( mToolbar->widgetForAction( mConstructionModeAction ) ); + constructionModeToolButton->setPopupMode( QToolButton::MenuButtonPopup ); + constructionModeToolButton->setMenu( constructionSettingsMenu ); + constructionModeToolButton->setObjectName( QStringLiteral( "ConstructionModeButton" ) ); + qobject_cast< QToolButton *>( mToolbar->widgetForAction( mSettingsAction ) )->setPopupMode( QToolButton::InstantPopup ); mSettingsAction->setMenu( mCommonAngleActionsMenu ); mSettingsAction->setCheckable( true ); @@ -368,6 +411,12 @@ QgsAdvancedDigitizingDockWidget::QgsAdvancedDigitizingDockWidget( QgsMapCanvas * updateCapacity( true ); connect( QgsProject::instance(), &QgsProject::snappingConfigChanged, this, [ = ] { updateCapacity( true ); } ); + connect( QgsProject::instance(), &QgsProject::cleared, this, [ = ]() + { + mConstructionGuidesLayer.reset(); + } ); + connect( mMapCanvas, &QgsMapCanvas::destinationCrsChanged, this, [ = ] { updateConstructionGuidesCrs(); } ); + disable(); } @@ -672,6 +721,14 @@ void QgsAdvancedDigitizingDockWidget::setConstructionMode( bool enabled ) { mConstructionMode = enabled; mConstructionModeAction->setChecked( enabled ); + + if ( recordConstructionGuides() ) + { + if ( enabled && mCadPointList.size() > 1 ) + { + mConstructionGuideLine.addVertex( mCadPointList.at( 1 ) ); + } + } } void QgsAdvancedDigitizingDockWidget::settingsButtonTriggered( QAction *action ) @@ -1195,6 +1252,11 @@ bool QgsAdvancedDigitizingDockWidget::applyConstraints( QgsMapMouseEvent *e ) context.setCadPoints( mCadPointList ); context.setLockedSnapVertices( mLockedSnapVertices ); + if ( snapToConstructionGuides() ) + { + context.snappingUtils->addExtraSnapLayer( mConstructionGuidesLayer.get() ); + } + context.commonAngleConstraint.locked = !mMapCanvas->mapSettings().destinationCrs().isGeographic(); context.commonAngleConstraint.relative = context.angleConstraint.relative; context.commonAngleConstraint.value = mCommonAngleConstraint; @@ -1254,6 +1316,7 @@ bool QgsAdvancedDigitizingDockWidget::applyConstraints( QgsMapMouseEvent *e ) * the point is not linked to a layer. */ e->setMapPoint( point ); + mSnapMatch = context.snappingUtils->snapToMap( point, nullptr, true ); if ( mSnapMatch.layer() ) { @@ -1265,6 +1328,8 @@ bool QgsAdvancedDigitizingDockWidget::applyConstraints( QgsMapMouseEvent *e ) } } + context.snappingUtils->removeExtraSnapLayer( mConstructionGuidesLayer.get() ); + if ( mSnapMatch.hasVertex() || mSnapMatch.hasLineEndpoint() ) { toggleLockedSnapVertex( mSnapMatch, mLastSnapMatch ); @@ -1501,6 +1566,11 @@ bool QgsAdvancedDigitizingDockWidget::canvasKeyPressEventFilter( QKeyEvent *e ) void QgsAdvancedDigitizingDockWidget::clear() { + if ( !mConstructionGuideLine.isEmpty() ) + { + mConstructionGuideLine.clear(); + } + clearPoints(); releaseLocks(); } @@ -1524,6 +1594,13 @@ void QgsAdvancedDigitizingDockWidget::keyPressEvent( QKeyEvent *e ) case Qt::Key_Escape: { releaseLocks(); + + if ( mConstructionGuideLine.numPoints() >= 2 ) + { + mConstructionGuidesLayer->dataProvider()->deleteFeatures( QgsFeatureIds() << mConstructionGuideId ); + mConstructionGuideLine.clear(); + } + break; } default: @@ -1575,6 +1652,22 @@ bool QgsAdvancedDigitizingDockWidget::filterKeyPress( QKeyEvent *e ) const QEvent::Type type = e->type(); switch ( e->key() ) { + case Qt::Key_Escape: + { + if ( mConstructionMode && mConstructionGuideLine.numPoints() >= 2 ) + { + mConstructionGuidesLayer->dataProvider()->deleteFeatures( QgsFeatureIds() << mConstructionGuideId ); + mConstructionGuideLine.clear(); + + if ( mCadPointList.size() > 1 ) + { + mConstructionGuideLine.addVertex( mCadPointList.at( 1 ) ); + } + + updateCadPaintItem(); + } + break; + } case Qt::Key_X: { // modifier+x ONLY caught for ShortcutOverride events... @@ -1856,7 +1949,20 @@ void QgsAdvancedDigitizingDockWidget::enable() { show(); } + setCadEnabled( mSessionActive ); + + if ( !mConstructionGuidesLayer ) + { + resetConstructionGuides(); + } + + if ( mDeferredUpdateConstructionGuidesCrs ) + { + updateConstructionGuidesCrs(); + } + + updateCadPaintItem(); } void QgsAdvancedDigitizingDockWidget::disable() @@ -1903,6 +2009,39 @@ void QgsAdvancedDigitizingDockWidget::addPoint( const QgsPointXY &point ) mCadPointList.insert( 0, pt ); } + if ( recordConstructionGuides() ) + { + if ( constructionMode() ) + { + mConstructionGuideLine.addVertex( pt ); + + if ( mConstructionGuideLine.numPoints() == 2 ) + { + QgsFeature feature; + QgsGeometry geom( mConstructionGuideLine.clone() ); + feature.setGeometry( geom ); + mConstructionGuidesLayer->dataProvider()->addFeature( feature ); + mConstructionGuideId = feature.id(); + } + else if ( mConstructionGuideLine.numPoints() > 2 ) + { + QgsGeometry geom( mConstructionGuideLine.clone() ); + mConstructionGuidesLayer->dataProvider()->changeGeometryValues( { { mConstructionGuideId, geom } } ); + } + } + else + { + if ( !mConstructionGuideLine.isEmpty() ) + { + mConstructionGuideLine.addVertex( pt ); + + QgsGeometry geom( mConstructionGuideLine.clone() ); + mConstructionGuidesLayer->dataProvider()->changeGeometryValues( { { mConstructionGuideId, geom } } ); + mConstructionGuideLine.clear(); + } + } + } + updateCapacity(); updateCadPaintItem(); } @@ -2146,3 +2285,58 @@ double QgsAdvancedDigitizingDockWidget::getLineM( ) const { return mMLineEdit->isEnabled() ? QLocale().toDouble( mMLineEdit->text() ) : std::numeric_limits::quiet_NaN(); } + +bool QgsAdvancedDigitizingDockWidget::showConstructionGuides() const +{ + return mShowConstructionGuides ? mShowConstructionGuides->isChecked() : false; +} + +bool QgsAdvancedDigitizingDockWidget::snapToConstructionGuides() const +{ + return mSnapToConstructionGuides ? mShowConstructionGuides->isChecked() && mSnapToConstructionGuides->isChecked() : false; +} + +bool QgsAdvancedDigitizingDockWidget::recordConstructionGuides() const +{ + return mRecordConstructionGuides ? mRecordConstructionGuides->isChecked() : false; +} + +void QgsAdvancedDigitizingDockWidget::updateConstructionGuidesCrs() +{ + if ( !mConstructionGuidesLayer ) + { + return; + } + + if ( !cadEnabled() ) + { + mDeferredUpdateConstructionGuidesCrs = true; + } + + QgsCoordinateTransform transform = QgsCoordinateTransform( mConstructionGuidesLayer->crs(), mMapCanvas->mapSettings().destinationCrs(), QgsProject::instance()->transformContext() ); + mConstructionGuidesLayer->setCrs( mMapCanvas->mapSettings().destinationCrs() ); + QgsFeatureIterator it = mConstructionGuidesLayer->getFeatures( QgsFeatureRequest().setNoAttributes() ); + QgsFeature feature; + while ( it.nextFeature( feature ) ) + { + QgsGeometry geom = feature.geometry(); + geom.transform( transform ); + mConstructionGuidesLayer->dataProvider()->changeGeometryValues( { { feature.id(), geom } } ); + } + + mDeferredUpdateConstructionGuidesCrs = false; +} + +void QgsAdvancedDigitizingDockWidget::resetConstructionGuides() +{ + if ( mConstructionGuidesLayer ) + { + mConstructionGuidesLayer.reset(); + } + + const QgsVectorLayer::LayerOptions options( QgsProject::instance()->transformContext(), false, false ); + mConstructionGuidesLayer = std::make_unique( QStringLiteral( "LineString?crs=%1" ).arg( mMapCanvas->mapSettings().destinationCrs().authid() ), + QStringLiteral( "constructionGuides" ), + QStringLiteral( "memory" ), + options ); +} diff --git a/src/gui/qgsadvanceddigitizingdockwidget.h b/src/gui/qgsadvanceddigitizingdockwidget.h index 4d76b4d216b8..13d3d7ecc3ed 100644 --- a/src/gui/qgsadvanceddigitizingdockwidget.h +++ b/src/gui/qgsadvanceddigitizingdockwidget.h @@ -305,9 +305,36 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private */ void setEnabledM( bool enable ); - //! construction mode is used to draw intermediate points. These points won't be given any further (i.e. to the map tools) + /** + * Returns whether the construction mode is activated. The construction mode is used to draw intermediate + * points that will not be part of a geometry being digitized. + */ bool constructionMode() const { return mConstructionMode; } + /** + * Returns the vector layer within which construction guides are stored. + * \since QGIS 3.40 + */ + QgsVectorLayer *constructionGuidesLayer() const { return mConstructionGuidesLayer.get(); } + + /** + * Returns whether the construction guides are visible. + * \since QGIS 3.40 + */ + bool showConstructionGuides() const; + + /** + * Returns whether points should snap to construction guides. + * \since QGIS 3.40 + */ + bool snapToConstructionGuides() const; + + /** + * Returns whether construction guides are being recorded. + * \since QGIS 3.40 + */ + bool recordConstructionGuides() const; + /** * Returns the between line constraints which are used to place * perpendicular/parallel segments to snapped segments on the canvas @@ -1013,7 +1040,6 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private //! Updates values of constraints that are not locked based on the current point void updateUnlockedConstraintValues( const QgsPoint &point ); - /** * Adds or removes the snap match if it is already in the locked snap queue or not. * \param snapMatch the snap match to add or remove. @@ -1021,6 +1047,17 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private */ void toggleLockedSnapVertex( const QgsPointLocator::Match &snapMatch, QgsPointLocator::Match previouslySnap ); + /** + * Resets the vector layer and point locator objects handling construction guides. + */ + void resetConstructionGuides(); + + /** + * Updates the construction guides layer CRS to match the map canvas' destination CRS + * and reproject pre-existing construction guides. + */ + void updateConstructionGuidesCrs(); + QgsMapCanvas *mMapCanvas = nullptr; QgsAdvancedDigitizingCanvasItem *mCadPaintItem = nullptr; //! Snapping indicator @@ -1053,19 +1090,29 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private //! Flag that controls whether snapping to features has priority over common angle bool mSnappingPrioritizeFeatures = false; - // point list and current snap point / segment + // Point list and current snap point / segment QList mCadPointList; QList mSnappedSegment; bool mSessionActive = false; - // error message + // Construction path history + std::unique_ptr mConstructionGuidesLayer; + QgsFeatureId mConstructionGuideId; + QgsLineString mConstructionGuideLine; + bool mDeferredUpdateConstructionGuidesCrs = false; + + // Error message std::unique_ptr mErrorMessage; // UI QMap< double, QAction *> mCommonAngleActions; // map the common angle actions with their angle values - QAction *mLineExtensionAction; - QAction *mXyVertexAction; + QAction *mLineExtensionAction = nullptr; + QAction *mXyVertexAction = nullptr; + QAction *mRecordConstructionGuides = nullptr; + QAction *mShowConstructionGuides = nullptr; + QAction *mSnapToConstructionGuides = nullptr; + QAction *mClearConstructionGuides = nullptr; // Snap indicator QgsPointLocator::Match mSnapMatch; @@ -1088,6 +1135,9 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private QMenu *mFloaterActionsMenu = nullptr; static const QgsSettingsEntryBool *settingsCadSnappingPriorityPrioritizeFeature; + static const QgsSettingsEntryBool *settingsCadRecordConstructionGuides; + static const QgsSettingsEntryBool *settingsCadShowConstructionGuides; + static const QgsSettingsEntryBool *settingsCadSnapToConstructionGuides; friend class TestQgsAdvancedDigitizing; friend class TestQgsAdvancedDigitizingDockWidget; diff --git a/tests/src/app/testqgsadvanceddigitizing.cpp b/tests/src/app/testqgsadvanceddigitizing.cpp index 8480f611cb83..8589ce67d79b 100644 --- a/tests/src/app/testqgsadvanceddigitizing.cpp +++ b/tests/src/app/testqgsadvanceddigitizing.cpp @@ -60,6 +60,8 @@ class TestQgsAdvancedDigitizing: public QObject void releaseLockAfterDisable(); + void constructionGuides(); + private: TestQgsMapToolAdvancedDigitizingUtils getMapToolDigitizingUtils( QgsVectorLayer *layer ); QString getWktFromLastAddedFeature( TestQgsMapToolAdvancedDigitizingUtils utils, QSet oldFeatures ); @@ -1163,5 +1165,55 @@ void TestQgsAdvancedDigitizing::releaseLockAfterDisable() mAdvancedDigitizingDockWidget->enableAction()->trigger(); } +void TestQgsAdvancedDigitizing::constructionGuides() +{ + auto utils = getMapToolDigitizingUtils( mLayer3950 ); + + QVERIFY( mAdvancedDigitizingDockWidget->cadEnabled() ); + + QCOMPARE( mAdvancedDigitizingDockWidget->constructionGuidesLayer()->featureCount(), 0 ); + + mAdvancedDigitizingDockWidget->mRecordConstructionGuides->setChecked( true ); + mAdvancedDigitizingDockWidget->setConstructionMode( true ); + + // enter a few construction steps while guide recording is on + utils.mouseClick( 10, 10, Qt::LeftButton ); + utils.mouseClick( 10, 11, Qt::LeftButton ); + utils.mouseClick( 10, 12, Qt::LeftButton ); + utils.mouseClick( 10, 13, Qt::LeftButton ); + utils.mouseClick( 10, 14, Qt::LeftButton ); + utils.mouseClick( 20, 01, Qt::RightButton ); + + QCOMPARE( mAdvancedDigitizingDockWidget->constructionGuidesLayer()->featureCount(), 1 ); + + mAdvancedDigitizingDockWidget->mRecordConstructionGuides->setChecked( false ); + + // enter a few construction steps while guide recording is off + utils.mouseClick( 10, 10, Qt::LeftButton ); + utils.mouseClick( 10, 11, Qt::LeftButton ); + utils.mouseClick( 10, 12, Qt::LeftButton ); + utils.mouseClick( 10, 13, Qt::LeftButton ); + utils.mouseClick( 10, 14, Qt::LeftButton ); + utils.mouseClick( 20, 01, Qt::RightButton ); + + QCOMPARE( mAdvancedDigitizingDockWidget->constructionGuidesLayer()->featureCount(), 1 ); + + QgsSnappingConfig snapConfig = mCanvas->snappingUtils()->config(); + snapConfig.setEnabled( true ); + mCanvas->snappingUtils()->setConfig( snapConfig ); + + mAdvancedDigitizingDockWidget->mSnapToConstructionGuides->setChecked( true ); + + // snap on an existing constructio guide vertex + utils.mouseMove( 10.1, 10 ); + QCOMPARE( mAdvancedDigitizingDockWidget->currentPointV2(), QgsPoint( 10, 10 ) ); + + mAdvancedDigitizingDockWidget->mSnapToConstructionGuides->setChecked( false ); + + // do not snap on an existing construction guide vertex + utils.mouseMove( 10.5, 14.5 ); + QGSCOMPARENEARPOINT( mAdvancedDigitizingDockWidget->currentPointV2(), QgsPoint( 10.5, 14.5 ), 0.1 ); +} + QGSTEST_MAIN( TestQgsAdvancedDigitizing ) #include "testqgsadvanceddigitizing.moc"