From 4274ad8842fe19caa993446c2726feced97f7bf4 Mon Sep 17 00:00:00 2001 From: Stefanos Natsis Date: Fri, 17 Jan 2025 18:01:36 +0200 Subject: [PATCH] Add undo command classes for point cloud editing (#60111) * Added undo command for point cloud editing change attribute * review fixes --- .../qgs3dmaptoolpointcloudchangeattribute.cpp | 2 + src/core/CMakeLists.txt | 2 + src/core/pointcloud/qgscopcpointcloudindex.h | 1 + src/core/pointcloud/qgslazdecoder.cpp | 5 -- src/core/pointcloud/qgslazdecoder.h | 1 - .../pointcloud/qgspointcloudeditingindex.h | 1 + src/core/pointcloud/qgspointcloudlayer.cpp | 54 +++++++++--- .../qgspointcloudlayereditutils.cpp | 71 +--------------- .../pointcloud/qgspointcloudlayereditutils.h | 20 +---- .../qgspointcloudlayerundocommand.cpp | 84 +++++++++++++++++++ .../qgspointcloudlayerundocommand.h | 75 +++++++++++++++++ tests/src/core/testqgspointcloudediting.cpp | 22 ++++- 12 files changed, 234 insertions(+), 104 deletions(-) create mode 100644 src/core/pointcloud/qgspointcloudlayerundocommand.cpp create mode 100644 src/core/pointcloud/qgspointcloudlayerundocommand.h diff --git a/src/app/3d/qgs3dmaptoolpointcloudchangeattribute.cpp b/src/app/3d/qgs3dmaptoolpointcloudchangeattribute.cpp index 660ce16968c0..d8be90f85f52 100644 --- a/src/app/3d/qgs3dmaptoolpointcloudchangeattribute.cpp +++ b/src/app/3d/qgs3dmaptoolpointcloudchangeattribute.cpp @@ -140,10 +140,12 @@ void Qgs3DMapToolPointCloudChangeAttribute::run() int offset; const QgsPointCloudAttribute *attribute = pcLayer->attributes().find( mAttributeName, offset ); + pcLayer->undoStack()->beginMacro( tr( "Change attribute values" ) ); for ( auto it = sel.begin(); it != sel.end(); ++it ) { pcLayer->changeAttributeValue( it.key(), it.value(), *attribute, mNewValue ); } + pcLayer->undoStack()->endMacro(); } void Qgs3DMapToolPointCloudChangeAttribute::restart() diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 9e0d477f3d5e..86201bc0e0bd 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -875,6 +875,7 @@ set(QGIS_CORE_SRCS pointcloud/qgspointcloudrendererregistry.cpp pointcloud/qgspointcloudrgbrenderer.cpp pointcloud/qgspointcloudlayerexporter.cpp + pointcloud/qgspointcloudlayerundocommand.cpp pointcloud/expression/qgspointcloudexpression.cpp pointcloud/expression/qgspointcloudexpressionnode.cpp @@ -1742,6 +1743,7 @@ set(QGIS_CORE_HDRS pointcloud/qgspointcloudrendererregistry.h pointcloud/qgspointcloudrgbrenderer.h pointcloud/qgspointcloudlayerexporter.h + pointcloud/qgspointcloudlayerundocommand.h pointcloud/expression/qgspointcloudexpression.h pointcloud/expression/qgspointcloudexpressionnode.h diff --git a/src/core/pointcloud/qgscopcpointcloudindex.h b/src/core/pointcloud/qgscopcpointcloudindex.h index 1ca11367d1eb..7b8b0459a999 100644 --- a/src/core/pointcloud/qgscopcpointcloudindex.h +++ b/src/core/pointcloud/qgscopcpointcloudindex.h @@ -124,6 +124,7 @@ class CORE_EXPORT QgsCopcPointCloudIndex: public QgsAbstractPointCloudIndex friend class QgsPointCloudLayerEditUtils; friend class QgsPointCloudEditingIndex; + friend class QgsPointCloudLayerUndoCommandChangeAttribute; }; ///@endcond diff --git a/src/core/pointcloud/qgslazdecoder.cpp b/src/core/pointcloud/qgslazdecoder.cpp index ecda055d01dc..f78dc5cdf0ba 100644 --- a/src/core/pointcloud/qgslazdecoder.cpp +++ b/src/core/pointcloud/qgslazdecoder.cpp @@ -186,11 +186,6 @@ bool lazSerialize_( char *data, size_t outputPosition, QgsPointCloudAttribute::D return true; } -bool lazStoreDoubleToStream( char *s, size_t position, QgsPointCloudAttribute::DataType type, double value ) -{ - return lazStoreToStream_( s, position, type, value ); -} - // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::vector< QgsLazDecoder::RequestedAttributeDetails > prepareRequestedAttributeDetails_( const QgsPointCloudAttributeCollection &requestedAttributes, QVector &extrabytesAttr ) diff --git a/src/core/pointcloud/qgslazdecoder.h b/src/core/pointcloud/qgslazdecoder.h index 03361c949e6e..bb49e7703cf1 100644 --- a/src/core/pointcloud/qgslazdecoder.h +++ b/src/core/pointcloud/qgslazdecoder.h @@ -37,7 +37,6 @@ template bool lazStoreToStream_( char *s, size_t position, QgsPointCloudAttribute::DataType type, T value ); bool lazSerialize_( char *data, size_t outputPosition, QgsPointCloudAttribute::DataType outputType, const char *input, QgsPointCloudAttribute::DataType inputType, int inputSize, size_t inputPosition ); -bool lazStoreDoubleToStream( char *s, size_t position, QgsPointCloudAttribute::DataType type, double value ); class QgsLazDecoder { diff --git a/src/core/pointcloud/qgspointcloudeditingindex.h b/src/core/pointcloud/qgspointcloudeditingindex.h index 0517cb9474e5..f60541796513 100644 --- a/src/core/pointcloud/qgspointcloudeditingindex.h +++ b/src/core/pointcloud/qgspointcloudeditingindex.h @@ -73,6 +73,7 @@ class CORE_EXPORT QgsPointCloudEditingIndex : public QgsAbstractPointCloudIndex QHash mEditedNodeData; friend class QgsPointCloudLayerEditUtils; + friend class QgsPointCloudLayerUndoCommandChangeAttribute; }; #endif // QGSPOINTCLOUDEDITINGINDEX_H diff --git a/src/core/pointcloud/qgspointcloudlayer.cpp b/src/core/pointcloud/qgspointcloudlayer.cpp index 8a14e8d7b672..ed15d8729f39 100644 --- a/src/core/pointcloud/qgspointcloudlayer.cpp +++ b/src/core/pointcloud/qgspointcloudlayer.cpp @@ -43,6 +43,7 @@ #include "qgstaskmanager.h" #include "qgsthreadingutils.h" #include "qgspointcloudlayerprofilegenerator.h" +#include "qgspointcloudlayerundocommand.h" #ifdef HAVE_COPC #include "qgscopcpointcloudindex.h" #endif @@ -73,6 +74,8 @@ QgsPointCloudLayer::QgsPointCloudLayer( const QString &uri, setLegend( QgsMapLayerLegend::defaultPointCloudLegend( this ) ); connect( this, &QgsPointCloudLayer::subsetStringChanged, this, &QgsMapLayer::configChanged ); + connect( undoStack(), &QUndoStack::indexChanged, this, &QgsMapLayer::layerModified ); + connect( this, &QgsMapLayer::layerModified, this, [this] { triggerRepaint(); } ); } QgsPointCloudLayer::~QgsPointCloudLayer() @@ -1034,11 +1037,7 @@ bool QgsPointCloudLayer::rollBack() if ( !mEditIndex ) return false; - if ( isModified() ) - { - emit layerModified(); - triggerRepaint(); - } + undoStack()->clear(); mEditIndex = QgsPointCloudIndex(); emit editingStopped(); @@ -1070,23 +1069,52 @@ bool QgsPointCloudLayer::isModified() const return mEditIndex.isModified(); } -bool QgsPointCloudLayer::changeAttributeValue( const QgsPointCloudNodeId &n, const QVector &pts, const QgsPointCloudAttribute &attribute, double value ) +bool QgsPointCloudLayer::changeAttributeValue( const QgsPointCloudNodeId &n, const QVector &points, const QgsPointCloudAttribute &attribute, double value ) { QGIS_PROTECT_QOBJECT_THREAD_ACCESS if ( !mEditIndex ) return false; - QgsPointCloudLayerEditUtils utils( this ); + // Cannot allow x,y,z editing as points may get moved outside the node extents + if ( attribute.name().compare( QLatin1String( "X" ), Qt::CaseInsensitive ) == 0 || + attribute.name().compare( QLatin1String( "Y" ), Qt::CaseInsensitive ) == 0 || + attribute.name().compare( QLatin1String( "Z" ), Qt::CaseInsensitive ) == 0 ) + return false; + + if ( !n.isValid() || !mEditIndex.hasNode( n ) ) // todo: should not have to check if n.isValid + return false; + + if ( points.isEmpty() ) + return false; + + const QgsPointCloudAttributeCollection attributeCollection = mEditIndex.attributes(); + + int attributeOffset; + const QgsPointCloudAttribute *at = attributeCollection.find( attribute.name(), attributeOffset ); - const bool success = utils.changeAttributeValue( n, pts, attribute, value ); - if ( success ) + if ( !at || + at->size() != attribute.size() || + at->type() != attribute.type() ) { - emit layerModified(); - emit triggerRepaint(); - emit trigger3DUpdate(); + return false; + } + + if ( !QgsPointCloudLayerEditUtils::isAttributeValueValid( attribute, value ) ) + { + return false; } - return success; + QVector sortedPoints( points.constBegin(), points.constEnd() ); + std::sort( sortedPoints.begin(), sortedPoints.end() ); + sortedPoints.erase( std::unique( sortedPoints.begin(), sortedPoints.end() ), sortedPoints.end() ); + + if ( sortedPoints.constFirst() < 0 || + sortedPoints.constLast() >= mEditIndex.getNode( n ).pointCount() ) + return false; + + undoStack()->push( new QgsPointCloudLayerUndoCommandChangeAttribute( mEditIndex, n, sortedPoints, attribute, value ) ); + + return true; } QgsPointCloudIndex QgsPointCloudLayer::index() const diff --git a/src/core/pointcloud/qgspointcloudlayereditutils.cpp b/src/core/pointcloud/qgspointcloudlayereditutils.cpp index f7995a76167a..91a5fbf57034 100644 --- a/src/core/pointcloud/qgspointcloudlayereditutils.cpp +++ b/src/core/pointcloud/qgspointcloudlayereditutils.cpp @@ -14,76 +14,13 @@ ***************************************************************************/ #include "qgspointcloudlayereditutils.h" -#include "qgspointcloudlayer.h" #include "qgslazdecoder.h" #include "qgscopcpointcloudindex.h" -#include "qgspointcloudeditingindex.h" #include #include -QgsPointCloudLayerEditUtils::QgsPointCloudLayerEditUtils( QgsPointCloudLayer *layer ) - : mIndex( layer->index() ) -{ -} - -bool QgsPointCloudLayerEditUtils::changeAttributeValue( const QgsPointCloudNodeId &n, const QVector &pts, const QgsPointCloudAttribute &attribute, double value ) -{ - // Cannot allow x,y,z editing as points may get moved outside the node extents - if ( attribute.name().compare( QLatin1String( "X" ), Qt::CaseInsensitive ) == 0 || - attribute.name().compare( QLatin1String( "Y" ), Qt::CaseInsensitive ) == 0 || - attribute.name().compare( QLatin1String( "Z" ), Qt::CaseInsensitive ) == 0 ) - return false; - - if ( !n.isValid() || !mIndex.hasNode( n ) ) // todo: should not have to check if n.isValid - return false; - - const QgsPointCloudAttributeCollection attributeCollection = mIndex.attributes(); - - int attributeOffset; - const QgsPointCloudAttribute *at = attributeCollection.find( attribute.name(), attributeOffset ); - - if ( !at || - at->size() != attribute.size() || - at->type() != attribute.type() ) - { - return false; - } - - if ( !isAttributeValueValid( attribute, value ) ) - { - return false; - } - - const QSet uniquePoints( pts.constBegin(), pts.constEnd() ); - QVector sortedPoints( uniquePoints.constBegin(), uniquePoints.constEnd() ); - std::sort( sortedPoints.begin(), sortedPoints.end() ); - - if ( sortedPoints.constFirst() < 0 || - sortedPoints.constLast() > mIndex.getNode( n ).pointCount() ) - return false; - - QgsPointCloudEditingIndex *editIndex = static_cast( mIndex.get() ); - QgsCopcPointCloudIndex *copcIndex = static_cast( editIndex->mIndex.get() ); - - QByteArray chunkData; - if ( editIndex->mEditedNodeData.contains( n ) ) - { - chunkData = editIndex->mEditedNodeData[n]; - } - else - { - QPair offsetSizePair = copcIndex->mHierarchyNodePos[n]; - chunkData = copcIndex->readRange( offsetSizePair.first, offsetSizePair.second ); - } - - QByteArray data = updateChunkValues( copcIndex, chunkData, *at, value, n, pts ); - - return mIndex.updateNodeData( {{n, data}} ); -} - - static void updatePoint( char *pointBuffer, int pointFormat, const QString &attributeName, double newValue ) { if ( attributeName == QLatin1String( "Intensity" ) ) // unsigned short @@ -187,7 +124,7 @@ static void updatePoint( char *pointBuffer, int pointFormat, const QString &attr } -QByteArray QgsPointCloudLayerEditUtils::updateChunkValues( QgsCopcPointCloudIndex *copcIndex, const QByteArray &chunkData, const QgsPointCloudAttribute &attribute, double newValue, const QgsPointCloudNodeId &n, const QVector &pointIndices ) +QByteArray QgsPointCloudLayerEditUtils::updateChunkValues( QgsCopcPointCloudIndex *copcIndex, const QByteArray &chunkData, const QgsPointCloudAttribute &attribute, const QgsPointCloudNodeId &n, const QHash &pointValues, std::optional newValue ) { Q_ASSERT( copcIndex->mHierarchy.contains( n ) ); Q_ASSERT( copcIndex->mHierarchyNodePos.contains( n ) ); @@ -204,8 +141,6 @@ QByteArray QgsPointCloudLayerEditUtils::updateChunkValues( QgsCopcPointCloudInde // only PDRF 6/7/8 is allowed by COPC Q_ASSERT( header.pointFormat() == 6 || header.pointFormat() == 7 || header.pointFormat() == 8 ); - QSet pointIndicesSet( pointIndices.constBegin(), pointIndices.constEnd() ); - QString attributeName = attribute.name(); for ( int i = 0 ; i < pointCount; ++i ) @@ -213,10 +148,10 @@ QByteArray QgsPointCloudLayerEditUtils::updateChunkValues( QgsCopcPointCloudInde decompressor.decompress( decodedData.get() ); char *buf = decodedData.get(); - if ( pointIndicesSet.contains( i ) ) + if ( pointValues.contains( i ) ) { // TODO: support for extrabytes attributes - updatePoint( buf, header.point_format_id, attributeName, newValue ); + updatePoint( buf, header.point_format_id, attributeName, newValue ? *newValue : pointValues[i] ); } compressor.compress( decodedData.get() ); diff --git a/src/core/pointcloud/qgspointcloudlayereditutils.h b/src/core/pointcloud/qgspointcloudlayereditutils.h index f94ba1720d9d..22bd90b819ce 100644 --- a/src/core/pointcloud/qgspointcloudlayereditutils.h +++ b/src/core/pointcloud/qgspointcloudlayereditutils.h @@ -19,6 +19,8 @@ #include "qgis_core.h" #include "qgspointcloudindex.h" +#include + #include #include @@ -45,18 +47,7 @@ class CORE_EXPORT QgsPointCloudLayerEditUtils { public: //! Ctor - QgsPointCloudLayerEditUtils( QgsPointCloudLayer *layer ); - - /** - * Attempts to modify attribute values for specific points in the editing buffer. - * - * \param n The point cloud node containing the points - * \param points The point ids of the points to be modified - * \param attribute The attribute whose value will be updated - * \param value The new value to set to the attribute - * \return TRUE if the editing buffer was updated successfully, FALSE otherwise - */ - bool changeAttributeValue( const QgsPointCloudNodeId &n, const QVector &points, const QgsPointCloudAttribute &attribute, double value ); + QgsPointCloudLayerEditUtils() = delete; //! Takes \a data comprising of \a allAttributes and returns a QByteArray with data only for the attributes included in the \a request static QByteArray dataForAttributes( const QgsPointCloudAttributeCollection &allAttributes, const QByteArray &data, const QgsPointCloudRequest &request ); @@ -64,12 +55,9 @@ class CORE_EXPORT QgsPointCloudLayerEditUtils //! Check if \a value is within proper range for the \a attribute static bool isAttributeValueValid( const QgsPointCloudAttribute &attribute, double value ); - private: - //! Sets new classification value for the given points in voxel and return updated chunk data - QByteArray updateChunkValues( QgsCopcPointCloudIndex *copcIndex, const QByteArray &chunkData, const QgsPointCloudAttribute &attribute, double newClassValue, const QgsPointCloudNodeId &n, const QVector &pointIndices ); + static QByteArray updateChunkValues( QgsCopcPointCloudIndex *copcIndex, const QByteArray &chunkData, const QgsPointCloudAttribute &attribute, const QgsPointCloudNodeId &n, const QHash &pointValues, std::optional newValue = std::nullopt ); - QgsPointCloudIndex mIndex; }; #endif // QGSPOINTCLOUDLAYEREDITUTILS_H diff --git a/src/core/pointcloud/qgspointcloudlayerundocommand.cpp b/src/core/pointcloud/qgspointcloudlayerundocommand.cpp new file mode 100644 index 000000000000..3cd690126a06 --- /dev/null +++ b/src/core/pointcloud/qgspointcloudlayerundocommand.cpp @@ -0,0 +1,84 @@ +/*************************************************************************** + qgspointcloudlayerundocommand.cpp + --------------------- + begin : January 2025 + copyright : (C) 2025 by Stefanos Natsis + email : uclaros at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgspointcloudlayerundocommand.h" +#include "qgspointcloudeditingindex.h" +#include "qgscopcpointcloudindex.h" +#include "qgspointcloudlayereditutils.h" + + +QgsPointCloudLayerUndoCommand::QgsPointCloudLayerUndoCommand( QgsPointCloudIndex index ) + : mIndex( index ) +{} + +QgsPointCloudLayerUndoCommandChangeAttribute::QgsPointCloudLayerUndoCommandChangeAttribute( QgsPointCloudIndex index, const QgsPointCloudNodeId &n, const QVector &points, const QgsPointCloudAttribute &attribute, double value ) + : QgsPointCloudLayerUndoCommand( index ) + , mNode( n ) + , mAttribute( attribute ) + , mNewValue( value ) +{ + const QgsPointCloudAttributeCollection allAttributes = mIndex.attributes(); + QgsPointCloudRequest req; + req.setAttributes( allAttributes ); + std::unique_ptr block = mIndex.nodeData( n, req ); + const char *ptr = block->data(); + block->attributes().find( attribute.name(), mAttributeOffset ); + const int size = block->pointRecordSize(); + for ( const int point : points ) + { + const int offset = point * size + mAttributeOffset; + const double oldValue = attribute.convertValueToDouble( ptr + offset ); + mPointValues[point] = oldValue; + } +} + +void QgsPointCloudLayerUndoCommandChangeAttribute::undo() +{ + undoRedoPrivate( true ); +} + +void QgsPointCloudLayerUndoCommandChangeAttribute::redo() +{ + undoRedoPrivate( false ); +} + +void QgsPointCloudLayerUndoCommandChangeAttribute::undoRedoPrivate( bool isUndo ) +{ + QgsPointCloudEditingIndex *editIndex = static_cast( mIndex.get() ); + QgsCopcPointCloudIndex *copcIndex = static_cast( editIndex->mIndex.get() ); + + QByteArray chunkData; + if ( editIndex->mEditedNodeData.contains( mNode ) ) + { + chunkData = editIndex->mEditedNodeData[mNode]; + } + else + { + QPair offsetSizePair = copcIndex->mHierarchyNodePos[mNode]; + chunkData = copcIndex->readRange( offsetSizePair.first, offsetSizePair.second ); + } + + QByteArray data; + if ( isUndo ) + { + data = QgsPointCloudLayerEditUtils::updateChunkValues( copcIndex, chunkData, mAttribute, mNode, mPointValues ); + } + else + { + data = QgsPointCloudLayerEditUtils::updateChunkValues( copcIndex, chunkData, mAttribute, mNode, mPointValues, mNewValue ); + } + + mIndex.updateNodeData( {{mNode, data}} ); +} diff --git a/src/core/pointcloud/qgspointcloudlayerundocommand.h b/src/core/pointcloud/qgspointcloudlayerundocommand.h new file mode 100644 index 000000000000..d6bba1601016 --- /dev/null +++ b/src/core/pointcloud/qgspointcloudlayerundocommand.h @@ -0,0 +1,75 @@ +/*************************************************************************** + qgspointcloudlayerundocommand.h + --------------------- + begin : January 2025 + copyright : (C) 2025 by Stefanos Natsis + email : uclaros at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSPOINTCLOUDLAYERUNDOCOMMAND_H +#define QGSPOINTCLOUDLAYERUNDOCOMMAND_H + +#define SIP_NO_FILE + +#include "qgis_core.h" +#include "qgspointcloudindex.h" +#include "qgspointcloudattribute.h" + +#include + +/** + * \ingroup core + * + * \brief Base class for undo/redo command for point cloud editing + * + * \since QGIS 3.42 + */ +class CORE_EXPORT QgsPointCloudLayerUndoCommand : public QUndoCommand +{ + protected: + //! Ctor + QgsPointCloudLayerUndoCommand( QgsPointCloudIndex index ); + QgsPointCloudIndex mIndex; +}; + +/** + * \ingroup core + * + * \brief An undo command subclass for changing point attribute values in a point cloud index + * + * \since QGIS 3.42 + */ +class CORE_EXPORT QgsPointCloudLayerUndoCommandChangeAttribute : public QgsPointCloudLayerUndoCommand +{ + public: + + /** + * Constructor for QgsPointCloudLayerUndoCommandChangeAttribute + * \param index associated point cloud index + * \param n the node id whose points will be modified + * \param points the list of points to be modified + * \param attribute the attribute whose value will be modified + * \param value the new value for the modified attribure + */ + QgsPointCloudLayerUndoCommandChangeAttribute( QgsPointCloudIndex index, const QgsPointCloudNodeId &n, const QVector &points, const QgsPointCloudAttribute &attribute, double value ); + + void undo() override; + void redo() override; + + private: + void undoRedoPrivate( bool isUndo ); + + QgsPointCloudNodeId mNode; + QHash< int, double > mPointValues; // contains pairs of (point number, old value) + QgsPointCloudAttribute mAttribute; + int mAttributeOffset; + double mNewValue; +}; +#endif // QGSPOINTCLOUDLAYERUNDOCOMMAND_H diff --git a/tests/src/core/testqgspointcloudediting.cpp b/tests/src/core/testqgspointcloudediting.cpp index 73100e59654f..ffe0ef5c6b8d 100644 --- a/tests/src/core/testqgspointcloudediting.cpp +++ b/tests/src/core/testqgspointcloudediting.cpp @@ -184,6 +184,7 @@ void TestQgsPointCloudEditing::testModifyAttributeValue() QVERIFY( layer->startEditing() ); QVERIFY( layer->isEditable() ); + QCOMPARE( layer->undoStack()->index(), 0 ); // Change some points, point order should not matter QgsPointCloudAttribute at( QStringLiteral( "Classification" ), QgsPointCloudAttribute::UChar ); @@ -191,19 +192,35 @@ void TestQgsPointCloudEditing::testModifyAttributeValue() QVERIFY( layer->changeAttributeValue( n, { 4, 2, 0, 1, 3, 16, 5, 13, 15, 14 }, at, 1 ) ); QVERIFY( layer->isModified() ); QCOMPARE( spy.size(), 1 ); + QCOMPARE( layer->undoStack()->index(), 1 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "classified_render_edit_1", "classified_render_edit_1", mapSettings ); // Change some more QVERIFY( layer->changeAttributeValue( n, { 42, 82, 62, 52, 72 }, at, 6 ) ); QVERIFY( layer->isModified() ); QCOMPARE( spy.size(), 2 ); + QCOMPARE( layer->undoStack()->index(), 2 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "classified_render_edit_2", "classified_render_edit_2", mapSettings ); + + // Undo one edit + layer->undoStack()->undo(); + QCOMPARE( spy.size(), 3 ); + QCOMPARE( layer->undoStack()->index(), 1 ); + QGSVERIFYRENDERMAPSETTINGSCHECK( "classified_render_edit_1", "classified_render_edit_1", mapSettings ); + + // Redo edit + layer->undoStack()->redo(); + QCOMPARE( spy.size(), 4 ); + QCOMPARE( layer->undoStack()->index(), 2 ); QGSVERIFYRENDERMAPSETTINGSCHECK( "classified_render_edit_2", "classified_render_edit_2", mapSettings ); // Abort editing, original points should be rendered QVERIFY( layer->rollBack() ); QVERIFY( !layer->isEditable() ); QVERIFY( !layer->isModified() ); - QCOMPARE( spy.size(), 3 ); + QCOMPARE( spy.size(), 5 ); + QCOMPARE( layer->undoStack()->index(), 0 ); QGSVERIFYRENDERMAPSETTINGSCHECK( "classified_render", "classified_render", mapSettings ); } @@ -215,6 +232,7 @@ void TestQgsPointCloudEditing::testModifyAttributeValueInvalid() QVERIFY( layer->isValid() ); QVERIFY( layer->startEditing() ); QVERIFY( layer->isEditable() ); + QCOMPARE( layer->undoStack()->index(), 0 ); QSignalSpy spy( layer.get(), &QgsMapLayer::layerModified ); @@ -408,6 +426,8 @@ void TestQgsPointCloudEditing::testModifyAttributeValueInvalid() QVERIFY( !layer->changeAttributeValue( n, { 42 }, at, 65536 ) ); QVERIFY( !layer->isModified() ); QCOMPARE( spy.size(), 0 ); + + QCOMPARE( layer->undoStack()->index(), 0 ); } void TestQgsPointCloudEditing::testCommitChanges()