Mesh transform by expression (#44873)

[feature] [mesh] allows the user to make geom transformation of mesh frame by changing the vertices coordinates by expression. Each coordinates (X,Y,Z) of selected vertices can be calculated with an expression allowing transformation of the mesh while the mesh is still valid.

Co-authored-by: Harrissou Sant-anna <delazj@gmail.com>
This commit is contained in:
Vincent Cloarec 2021-09-03 02:48:41 -04:00 committed by GitHub
parent 8b18399704
commit 9ce295ebbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1232 additions and 158 deletions

View File

@ -925,6 +925,7 @@
<file>themes/default/mActionMeshDigitizing.svg</file>
<file>themes/default/mActionMeshSelectPolygon.svg</file>
<file>themes/default/mActionNewMeshLayer.svg</file>
<file>themes/default/mActionMeshTransformByExpression.svg</file>
<file>themes/default/mIconGeometryCollectionLayer.svg</file>
<file>themes/default/mIconGps.svg</file>
<file>themes/default/mActionNewGpx.svg</file>

View File

@ -0,0 +1 @@
<svg height="24" width="24" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="#5b6775" stroke-linecap="round" stroke-width="1.001834"><path d="m21.881899 5.6699901-9.359749 1.0247509m4.044093-5.1770872 1.271563 9.3294242m-1.329362-9.7534888 5.798899 4.5298213-4.411738 5.6477305-5.798898-4.5298209z"/><path d="m10.499931 14.707888-7.887379 7.548378m0-7.548378 7.887379 7.548378m-8.2458959-7.891486h8.6044139v8.234593h-8.6044139z"/><path d="m9.2712536 2.6065281-6.2013467 6.2974513m0-6.2974513 6.2013467 6.2974513m-6.4832259-6.5836989h6.7651061v6.8699459h-6.7651061z"/></g><g transform="translate(5.318556 -3.59995)"><path d="m-12.901812 19.194933c-1.92383-.683586-2.885744-1.718741-2.885742-3.105469-.000002-1.093739.50781-1.967761 1.523438-2.62207 1.025386-.654284 2.294916-.981432 3.808593-.981445 1.3769451.000013 2.470694.229505 3.2812505.688476.8105357.449232 1.2158087.991223 1.2158203 1.625977-.0000116.332042-.1269646.625011-.3808594.878906-.2539172.244151-.5468856.366221-.8789062.366211-.5468848.00001-.9961031-.380849-1.3476563-1.142578-.4882897-1.054675-1.2207109-1.582019-2.1972659-1.582031-.77149.000012-1.406255.253918-1.904297.761718-.498051.507824-.747074 1.215832-.74707 2.124024-.000004 1.787117.922847 2.680671 2.768555 2.680664.195305.000007.419914-.01952.6738279-.05859.4394451-.05859.7812417-.08788 1.0253906-.08789.595694.000008.8935452.170906.8935547.512696-.0000095.380866-.3027435.571295-.9082031.571289-.2148521.000006-.5371174-.03417-.9667969-.102539-.3222732-.05859-.5712962-.08788-.7470702-.08789-1.95313.000007-2.929692.996099-2.929688 2.988281-.000004.9668.258785 1.748049.776368 2.34375.517572.585939 1.240228.878907 2.167968.878907 1.1621017 0 1.9335853-.600585 2.3144535-1.801758.1953031-.634763.4101466-1.074216.6445312-1.31836.2441306-.244136.5663959-.366207.9667969-.366211.3320201.000004.6298714.122075.8935547.366211.2734255.234379.4101441.537113.4101562.908204-.0000121.888673-.4980585 1.625977-1.4941406 2.211914-.9961034.576171-2.2021568.864257-3.6181639.864257-1.55274 0-2.934575-.371093-4.145508-1.113281-1.201173-.742186-1.801759-1.733396-1.801758-2.973633-.000001-1.552729 1.196287-2.695306 3.588867-3.427734" fill="#5c3566" transform="translate(22.960894 .402232)"/><g fill="#fff"><path d="m8.8453703 15.335963c-1.8135886 1.95573-1.401671-.322256-.3145916-1.106748 1.1264312-.81289 1.7573183-.79349 1.7490763-.54418-.0096.290414-.8636237 1.035325-1.4344847 1.650928z" opacity=".5"/><path d="m8.2348719 21.645433c-1.8135886 1.95573-1.5022297.147018-.4151503-.637474 1.1264312-.81289 1.7573183-.79349 1.7490763-.54418-.0096.290414-.763065.566051-1.333926 1.181654z" opacity=".5"/><path d="m16.204463 23.831019c-1.74795 1.952651-1.613829.09846-.49057-.620714.858053-.549374 1.112902-.0745.49057.620714z" opacity=".5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -83,6 +83,69 @@ Constructor
};
class QgsMeshTransformVerticesByExpression : QgsMeshAdvancedEditing
{
%Docstring(signature="appended")
Class that can transform vertices of a mesh by expression
Each coordinates are associated with an expression that can be defined with function
returning the current coordinates (see :py:func:`~QgsMeshEditRefineFaces.setExpressions`):
- $vertex_x
- $vertex_y
- $vertex_z
Example:
Transposing a mesh and translate following axe X with a distance of 50 and increase the level of the mesh
with an height of 80 when previous X coordinate is under 100 and de crease the level of 150 when X is under 100:
expressionX: "$vertex_y + 50"
expressionY: "$vertex_x"
expressionZ: "if( $vertex_x <= 100 , $vertex_z + 80 , $vertex_z - 150)"
.. versionadded:: 3.22
%End
%TypeHeaderCode
#include "qgsmeshadvancedediting.h"
%End
public:
QgsMeshTransformVerticesByExpression();
%Docstring
Constructor
%End
void setExpressions( const QString &expressionX, const QString &expressionY, const QString &expressionZ );
%Docstring
Sets the expressions for the coordinates transformation.
.. note::
Expressions are optional for each coordinate, the coordinate will not be transformed if the string is void.
%End
bool calculate( QgsMeshLayer *layer );
%Docstring
Calculates the transformed vertices of the mesh ``layer``, returns ``False`` if this leads to topological or geometrical errors.
The mesh layer must be in edit mode.
.. note::
this method not apply new vertices to the mesh layer but only store the calculated transformation
that can be apply later with :py:func:`QgsMeshEditor.advancedEdit()`
%End
QgsMeshVertex transformedVertex( QgsMeshLayer *layer, int vertexIndex ) const;
%Docstring
Returns the transformed vertex from its index ``vertexIndex`` for the mesh ``layer``
If ``layer`` is not the same than the one used to make the calculation, this will create an undefined behavior
%End
};
/************************************************************************
* This file has been generated automatically from *
* *

View File

@ -171,7 +171,7 @@ Returns whether the mesh has been modified
%End
QList<int> freeVerticesIndexes();
QList<int> freeVerticesIndexes() const;
%Docstring
Returns all the free vertices indexes
%End

View File

@ -248,6 +248,7 @@ set(QGIS_APP_SRCS
mesh/qgsmeshcalculatordialog.cpp
mesh/qgsnewmeshlayerdialog.cpp
mesh/qgsmaptooleditmeshframe.cpp
mesh/qgsmeshtransformcoordinatesdockwidget.cpp
)
if (WITH_SPATIALITE)

View File

@ -35,6 +35,7 @@
#include "qgsvertexmarker.h"
#include "qgsguiutils.h"
#include "qgsmeshtriangulation.h"
#include "qgsmeshtransformcoordinatesdockwidget.h"
QgsZValueWidget::QgsZValueWidget( const QString &label, QWidget *parent ): QWidget( parent )
@ -90,11 +91,14 @@ QgsMapToolEditMeshFrame::QgsMapToolEditMeshFrame( QgsMapCanvas *canvas )
: QgsMapToolAdvancedDigitizing( canvas, QgisApp::instance()->cadDockWidget() )
, mSnapIndicator( new QgsSnapIndicator( canvas ) )
{
mActionDigitizing = new QAction( QgsApplication::getThemePixmap( QStringLiteral( "/mActionMeshDigitizing.svg" ) ), tr( "Digitize mesh elements" ) );
mActionDigitizing = new QAction( QgsApplication::getThemePixmap( QStringLiteral( "/mActionMeshDigitizing.svg" ) ), tr( "Digitize mesh elements" ), this );
mActionDigitizing->setCheckable( true );
mActionSelectByPolygon = new QAction( QgsApplication::getThemePixmap( QStringLiteral( "/mActionMeshSelectPolygon.svg" ) ), tr( "Select mesh element by polygon" ) );
mActionSelectByPolygon = new QAction( QgsApplication::getThemePixmap( QStringLiteral( "/mActionMeshSelectPolygon.svg" ) ), tr( "Select mesh element by polygon" ), this );
mActionSelectByPolygon->setCheckable( true );
mActionTransformCoordinates = new QAction( QgsApplication::getThemePixmap( QStringLiteral( "/mActionMeshTransformByExpression.svg" ) ), tr( "Transform vertices coordinates" ), this );
mActionTransformCoordinates->setCheckable( true );
mActionRemoveVerticesFillingHole = new QAction( this );
mActionDelaunayTriangulation = new QAction( tr( "Delaunay triangulation with selected vertices" ), this );
mActionFacesRefinement = new QAction( tr( "Refine current face" ), this );
@ -149,6 +153,8 @@ QgsMapToolEditMeshFrame::QgsMapToolEditMeshFrame( QgsMapCanvas *canvas )
}
} );
connect( mActionTransformCoordinates, &QAction::triggered, this, &QgsMapToolEditMeshFrame::triggerTransformCoordinatesDockWidget );
setAutoSnapEnabled( true );
}
@ -177,7 +183,8 @@ QList<QAction *> QgsMapToolEditMeshFrame::actions() const
{
return QList<QAction *>()
<< mActionDigitizing
<< mActionSelectByPolygon;
<< mActionSelectByPolygon
<< mActionTransformCoordinates;
}
QList<QAction *> QgsMapToolEditMeshFrame::mapToolActions()
@ -275,14 +282,6 @@ void QgsMapToolEditMeshFrame::initialize()
if ( !mMovingFacesRubberband )
mMovingFacesRubberband = createRubberBand( QgsWkbTypes::PolygonGeometry );
if ( !mMovingVerticesRubberband )
mMovingVerticesRubberband = createRubberBand( QgsWkbTypes::PointGeometry );
mMovingEdgesRubberband->setWidth( QgsGuiUtils::scaleIconSize( 2 ) );
mMovingEdgesRubberband->setBrushStyle( Qt::NoBrush );
mMovingEdgesRubberband->setIconSize( QgsGuiUtils::scaleIconSize( 10 ) );
mMovingEdgesRubberband->setVisible( false );
mMovingEdgesRubberband->setZValue( 5 );
if ( !mFlipEdgeMarker )
mFlipEdgeMarker = new QgsVertexMarker( canvas() );
mFlipEdgeMarker->setIconType( QgsVertexMarker::ICON_CIRCLE );
@ -552,13 +551,13 @@ void QgsMapToolEditMeshFrame::cadCanvasMoveEvent( QgsMapMouseEvent *e )
{
const QgsVector &translation = mapPoint - mStartMovingPoint;
mMovingEdgesRubberband->reset( QgsWkbTypes::LineGeometry );
mMovingVerticesRubberband->reset( QgsWkbTypes::PointGeometry );
mMovingFacesRubberband->reset( QgsWkbTypes::PolygonGeometry );
QgsGeometry faceGeom = mSelectedFacesRubberband->asGeometry();
faceGeom.translate( translation.x(), translation.y() );
mMovingFacesRubberband->setToGeometry( faceGeom );
QgsGeometry movingFacesGeometry = mSelectedFacesRubberband->asGeometry();
movingFacesGeometry.translate( translation.x(), translation.y() );
mMovingFacesRubberband->setToGeometry( movingFacesGeometry );
QSet<int> borderMovingFace;
mIsMovingAllowed = true;
for ( QMap<int, SelectedVertexData>::const_iterator it = mSelectedVertices.constBegin(); it != mSelectedVertices.constEnd(); ++it )
{
const QgsPointXY &point1 = mapVertexXY( it.key() ) + translation;
@ -568,32 +567,46 @@ void QgsMapToolEditMeshFrame::cadCanvasMoveEvent( QgsMapMouseEvent *e )
const QgsPointXY point2 = mapVertexXY( vertexData.meshFixedEdges.at( i ).second );
QgsGeometry edge( new QgsLineString( {point1, point2} ) );
mMovingEdgesRubberband->addGeometry( edge );
if ( mIsMovingAllowed )
mIsMovingAllowed &= testBorderMovingFace( nativeFace( vertexData.meshFixedEdges.at( i ).first ), translation );
int associateFace = vertexData.meshFixedEdges.at( i ).first;
if ( associateFace != -1 )
borderMovingFace.insert( associateFace );
}
for ( int i = 0; i < vertexData.selectedEdges.count(); ++i )
for ( int i = 0; i < vertexData.borderEdges.count(); ++i )
{
const QgsPointXY point2 = mapVertexXY( vertexData.selectedEdges.at( i ).second ) + translation;
const QgsPointXY middlePoint( ( point1.x() + point2.x() ) / 2, ( point1.y() + point2.y() ) / 2 );
if ( !faceGeom.contains( &middlePoint ) )
{
QgsGeometry edge( new QgsLineString( {point1, point2} ) );
mMovingEdgesRubberband->addGeometry( edge );
}
const QgsPointXY point2 = mapVertexXY( vertexData.borderEdges.at( i ).second ) + translation;
const QgsGeometry edge( new QgsLineString( {point1, point2} ) );
mMovingEdgesRubberband->addGeometry( edge );
}
}
const QgsMeshVertex &mapPointInNativeCoordinate =
mCurrentLayer->triangularMesh()->triangularToNativeCoordinates( QgsMeshVertex( mapPoint.x(), mapPoint.y() ) );
const QgsMeshVertex &startingPointInNativeCoordinate =
mCurrentLayer->triangularMesh()->triangularToNativeCoordinates( QgsMeshVertex( mStartMovingPoint.x(), mStartMovingPoint.y() ) );
const QgsVector &translationInLayerCoordinate = mapPointInNativeCoordinate - startingPointInNativeCoordinate;
auto transformFunction = [translationInLayerCoordinate, this ]( int vi )-> const QgsMeshVertex
{
if ( mSelectedVertices.contains( vi ) )
return mCurrentLayer->nativeMesh()->vertex( vi ) + translationInLayerCoordinate;
else
return mCurrentLayer->nativeMesh()->vertex( vi );
};
// we test only the faces that are deformed on the border, moving and not deformed faces are tested later
mIsMovingAllowed = mCurrentEditor->canBeTransformed( qgis::setToList( borderMovingFace ), transformFunction );
if ( mIsMovingAllowed )
{
//to finish test if the polygons formed by the moving faces contains something else
const QList<int> &faceIndexesIntersect = mCurrentLayer->triangularMesh()->nativeFaceIndexForRectangle( faceGeom.boundingBox() );
const QList<int> &faceIndexesIntersect = mCurrentLayer->triangularMesh()->nativeFaceIndexForRectangle( movingFacesGeometry.boundingBox() );
for ( const int faceIndex : faceIndexesIntersect )
{
if ( mConcernedFaceBySelection.contains( faceIndex ) )
continue;
const QgsGeometry otherFaceGeom( new QgsPolygon( new QgsLineString( nativeFaceGeometry( faceIndex ) ) ) );
mIsMovingAllowed &= !faceGeom.intersects( otherFaceGeom );
mIsMovingAllowed &= !movingFacesGeometry.intersects( otherFaceGeom );
if ( !mIsMovingAllowed )
break;
}
@ -604,23 +617,14 @@ void QgsMapToolEditMeshFrame::cadCanvasMoveEvent( QgsMapMouseEvent *e )
for ( const int vertexIndex : freeVerticesIndexes )
{
const QgsPointXY &pointInMap = mapVertexXY( vertexIndex );
mIsMovingAllowed &= !faceGeom.contains( &pointInMap );
mIsMovingAllowed &= !movingFacesGeometry.contains( &pointInMap );
if ( !mIsMovingAllowed )
break;
}
}
}
if ( mIsMovingAllowed )
{
mMovingFacesRubberband->setFillColor( QColor( 0, 200, 0, 100 ) );
mMovingEdgesRubberband->setColor( QColor( 0, 200, 0, 100 ) );
}
else
{
mMovingFacesRubberband->setFillColor( QColor( 200, 0, 0, 100 ) );
mMovingEdgesRubberband->setColor( QColor( 200, 0, 0, 100 ) );
}
setMovingRubberBandValidity( mIsMovingAllowed );
}
break;
case SelectingByPolygon:
@ -751,11 +755,17 @@ void QgsMapToolEditMeshFrame::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
const QList<int> verticesIndexes = mSelectedVertices.keys();
QList<QgsPointXY> newPosition;
newPosition.reserve( verticesIndexes.count() );
QgsVector translation = e->mapPoint() - mStartMovingPoint;
const QgsMeshVertex &mapPointInNativeCoordinate =
mCurrentLayer->triangularMesh()->triangularToNativeCoordinates( QgsMeshVertex( mapPoint.x(), mapPoint.y() ) );
const QgsMeshVertex &startingPointInNativeCoordinate =
mCurrentLayer->triangularMesh()->triangularToNativeCoordinates( QgsMeshVertex( mStartMovingPoint.x(), mStartMovingPoint.y() ) );
const QgsVector &translationInLayerCoordinate = mapPointInNativeCoordinate - startingPointInNativeCoordinate;
const QgsMesh &mesh = *mCurrentLayer->nativeMesh();
for ( int i = 0; i < verticesIndexes.count(); ++i )
{
newPosition.append( mapVertexXY( verticesIndexes.at( i ) ) + translation );
}
newPosition.append( QgsPointXY( mesh.vertex( verticesIndexes.at( i ) ) ) + translationInLayerCoordinate );
mKeepSelectionOnEdit = true;
mCurrentEditor->changeXYValues( mSelectedVertices.keys(), newPosition );
}
@ -764,7 +774,6 @@ void QgsMapToolEditMeshFrame::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
clearCanvasHelpers();
mMovingEdgesRubberband->reset();
mMovingFacesRubberband->reset();
mMovingVerticesRubberband->reset();
mCurrentState = Digitizing;
break;
case SelectingByPolygon:
@ -815,92 +824,6 @@ void QgsMapToolEditMeshFrame::select( const QgsPointXY &mapPoint, Qt::KeyboardMo
setSelectedVertices( QList<int>(), modifiers );
}
bool QgsMapToolEditMeshFrame::testBorderMovingFace( const QgsMeshFace &borderMovingfaces, const QgsVector &translation ) const
{
int faceSize = borderMovingfaces.count();
QgsPolygonXY polygon;
QVector<QgsPointXY> points( faceSize );
for ( int i = 0; i < faceSize; ++i )
{
int ip0 = borderMovingfaces[i];
int ip1 = borderMovingfaces[( i + 1 ) % faceSize];
int ip2 = borderMovingfaces[( i + 2 ) % faceSize];
QgsPointXY p0 = mCurrentLayer->nativeMesh()->vertices.at( ip0 ) + ( mSelectedVertices.contains( ip0 ) ? translation : QgsVector( 0, 0 ) );
QgsPointXY p1 = mCurrentLayer->nativeMesh()->vertices.at( ip1 ) + ( mSelectedVertices.contains( ip1 ) ? translation : QgsVector( 0, 0 ) );
QgsPointXY p2 = mCurrentLayer->nativeMesh()->vertices.at( ip2 ) + ( mSelectedVertices.contains( ip2 ) ? translation : QgsVector( 0, 0 ) );
double ux = p0.x() - p1.x();
double uy = p0.y() - p1.y();
double vx = p2.x() - p1.x();
double vy = p2.y() - p1.y();
double crossProduct = ux * vy - uy * vx;
if ( crossProduct >= 0 ) //if cross product>0, we have two edges clockwise
return false;
points[i] = p0;
}
polygon.append( points );
const QgsGeometry &deformedFace = QgsGeometry::fromPolygonXY( polygon );
// now test if the deformedface contain something else
QList<int> otherFaceIndexes = mCurrentLayer->triangularMesh()->nativeFaceIndexForRectangle( deformedFace.boundingBox() );
{
for ( const int otherFaceIndex : otherFaceIndexes )
{
if ( mConcernedFaceBySelection.contains( otherFaceIndex ) )
continue;
const QgsMeshFace &otherFace = nativeFace( otherFaceIndex );
int existingFaceSize = otherFace.count();
bool shareVertex = false;
for ( int i = 0; i < existingFaceSize; ++i )
{
if ( borderMovingfaces.contains( otherFace.at( i ) ) )
{
shareVertex = true;
break;
}
}
if ( shareVertex )
{
//only test the edge that not contain a shared vertex
for ( int i = 0; i < existingFaceSize; ++i )
{
int index1 = otherFace.at( i );
int index2 = otherFace.at( ( i + 1 ) % existingFaceSize );
if ( ! borderMovingfaces.contains( index1 ) && !borderMovingfaces.contains( index2 ) )
{
const QgsPointXY &v1 = mapVertexXY( index1 );
const QgsPointXY &v2 = mapVertexXY( index2 );
QgsGeometry edgeGeom = QgsGeometry::fromPolylineXY( { v1, v2} );
if ( deformedFace.intersects( edgeGeom ) )
return false;
}
}
}
else
{
const QgsGeometry existingFaceGeom( new QgsPolygon( new QgsLineString( nativeFaceGeometry( otherFaceIndex ) ) ) );
if ( deformedFace.intersects( existingFaceGeom ) )
return false;
}
}
}
//finish with free vertices...
const QList<int> freeVerticesIndex = mCurrentEditor->freeVerticesIndexes();
for ( const int vertexIndex : freeVerticesIndex )
{
const QgsPointXY &mapPoint = mapVertexXY( vertexIndex );
if ( deformedFace.contains( &mapPoint ) )
return false;
}
return true;
}
void QgsMapToolEditMeshFrame::keyPressEvent( QKeyEvent *e )
{
bool consumned = false;
@ -1062,6 +985,9 @@ void QgsMapToolEditMeshFrame::setCurrentLayer( QgsMapLayer *layer )
if ( mCurrentLayer == meshLayer && mCurrentEditor != nullptr )
return;
if ( mIsInitialized )
clearSelection(); //TODO later: implement a mechanism to retrieve selection if the layer is again selected
if ( mCurrentLayer )
{
disconnect( mCurrentLayer, &QgsMeshLayer::editingStarted, this, &QgsMapToolEditMeshFrame::onEditingStarted );
@ -1101,6 +1027,7 @@ void QgsMapToolEditMeshFrame::setCurrentLayer( QgsMapLayer *layer )
else
deactivate();
emit selectionChange( mCurrentLayer, mSelectedVertices.keys() );
}
void QgsMapToolEditMeshFrame::onEdit()
@ -1291,6 +1218,146 @@ void QgsMapToolEditMeshFrame::splitSelectedFaces()
mCurrentEditor->splitFaces( {mCurrentFaceIndex} );
}
void QgsMapToolEditMeshFrame::triggerTransformCoordinatesDockWidget( bool checked )
{
if ( !checked && mTransformDockWidget )
{
mTransformDockWidget->deleteLater();
mTransformDockWidget = nullptr;
return;
}
else if ( mTransformDockWidget )
{
return;
}
onEditingStarted();
mTransformDockWidget = new QgsMeshTransformCoordinatesDockWidget( QgisApp::instance() );
mTransformDockWidget->setWindowTitle( tr( "Transform Mesh Vertices" ) );
mTransformDockWidget->setObjectName( QStringLiteral( "TransformMeshVerticesDockWidget" ) );
mTransformDockWidget->setAttribute( Qt::WA_DeleteOnClose );
mTransformDockWidget->setInput( mCurrentLayer, mSelectedVertices.keys() );
if ( !QgisApp::instance()->restoreDockWidget( mTransformDockWidget ) )
QgisApp::instance()->addDockWidget( Qt::LeftDockWidgetArea, mTransformDockWidget );
else
QgisApp::instance()->panelMenu()->addAction( mTransformDockWidget->toggleViewAction() );
mTransformDockWidget->show();
connect( this, &QgsMapToolEditMeshFrame::selectionChange, mTransformDockWidget, &QgsMeshTransformCoordinatesDockWidget::setInput );
connect( mTransformDockWidget, &QgsMeshTransformCoordinatesDockWidget::calculationUpdated, this, [this]
{
mMovingFacesRubberband->reset( QgsWkbTypes::PolygonGeometry );
mMovingEdgesRubberband->reset( QgsWkbTypes::LineGeometry );
setMovingRubberBandValidity( mTransformDockWidget->isResultValid() );
if ( !mCurrentLayer || !mCurrentEditor )
return;
QList<int> faceList = qgis::setToList( mSelectedFaces );
QgsGeometry faceGeometrie;
if ( faceList.count() == 1 )
{
const QgsMeshFace &face = mCurrentLayer->nativeMesh()->face( faceList.at( 0 ) );
const int faceSize = face.size();
QVector<QgsPointXY> faceVertices( faceSize );
for ( int j = 0; j < faceSize; ++j )
faceVertices[j] = mTransformDockWidget->transformedVertex( face.at( j ) );
faceGeometrie = QgsGeometry::fromPolygonXY( {faceVertices} );
}
else
{
std::unique_ptr<QgsGeometryEngine> geomEngine( QgsGeometry::createGeometryEngine( faceGeometrie.constGet() ) );
geomEngine->prepareGeometry();
QVector<QgsGeometry> faces( mSelectedFaces.count() );
for ( int i = 0; i < faceList.count(); ++i )
{
const QgsMeshFace &face = mCurrentLayer->nativeMesh()->face( faceList.at( i ) );
const int faceSize = face.size();
QVector<QgsPointXY> faceVertices( faceSize );
for ( int j = 0; j < faceSize; ++j )
faceVertices[j] = mTransformDockWidget->transformedVertex( face.at( j ) );
faces[i] = QgsGeometry::fromPolygonXY( {faceVertices} );
}
QString error;
faceGeometrie = QgsGeometry( geomEngine->combine( faces, &error ) );
}
QSet<int> borderMovingFace;
QgsGeometry edgesGeom = QgsGeometry::fromMultiPolylineXY( QgsMultiPolylineXY() );
for ( QMap<int, SelectedVertexData>::const_iterator it = mSelectedVertices.constBegin(); it != mSelectedVertices.constEnd(); ++it )
{
const QgsPointXY &point1 = mTransformDockWidget->transformedVertex( it.key() ) ;
const SelectedVertexData &vertexData = it.value();
for ( int i = 0; i < vertexData.meshFixedEdges.count(); ++i )
{
const QgsPointXY point2 = mTransformDockWidget->transformedVertex( vertexData.meshFixedEdges.at( i ).second );
QgsGeometry edge( new QgsLineString( {point1, point2} ) );
edgesGeom.addPart( edge );
int associateFace = vertexData.meshFixedEdges.at( i ).first;
if ( associateFace != -1 )
borderMovingFace.insert( associateFace );
}
for ( int i = 0; i < vertexData.borderEdges.count(); ++i )
{
const QgsPointXY point2 = mTransformDockWidget->transformedVertex( vertexData.borderEdges.at( i ).second );
const QgsGeometry edge( new QgsLineString( {point1, point2} ) );
edgesGeom.addPart( edge );
}
}
QgsCoordinateTransform coordinateTransform( mCurrentLayer->crs(), canvas()->mapSettings().destinationCrs(), QgsProject::instance() );
try
{
faceGeometrie.transform( coordinateTransform );
}
catch ( QgsCsException & )
{}
try
{
edgesGeom.transform( coordinateTransform );
}
catch ( QgsCsException & )
{}
mMovingFacesRubberband->setToGeometry( faceGeometrie );
mMovingEdgesRubberband->setToGeometry( edgesGeom );
setMovingRubberBandValidity( mTransformDockWidget->isResultValid() );
} );
connect( mTransformDockWidget, &QgsMeshTransformCoordinatesDockWidget::aboutToBeApplied, this, [this]
{
mKeepSelectionOnEdit = true;
} );
connect( mTransformDockWidget, &QgsMeshTransformCoordinatesDockWidget::applied, this, [this]
{
mTransformDockWidget->setInput( mCurrentLayer, mSelectedVertices.keys() );
updateSelectecVerticesMarker();
prepareSelection();
} );
connect( mTransformDockWidget, &QgsMeshTransformCoordinatesDockWidget::destroyed, this, [this]
{
mTransformDockWidget = nullptr;
mActionTransformCoordinates->setChecked( false );
if ( !mIsInitialized )
return;
mMovingFacesRubberband->reset( QgsWkbTypes::PolygonGeometry );
mMovingEdgesRubberband->reset( QgsWkbTypes::LineGeometry );
setMovingRubberBandValidity( false );
} );
}
void QgsMapToolEditMeshFrame::selectInGeometry( const QgsGeometry &geometry, Qt::KeyboardModifiers modifiers )
{
if ( mCurrentLayer.isNull() || !mCurrentLayer->triangularMesh() || mCurrentEditor.isNull() )
@ -1355,7 +1422,6 @@ void QgsMapToolEditMeshFrame::applyZValueOnSelectedVertices()
void QgsMapToolEditMeshFrame::prepareSelection()
{
mConcernedFaceBySelection.clear();
QSet<int> borderSelectionVertices;
QMap<int, SelectedVertexData> movingVertices;
// search for moving edges and mesh fixed edges
@ -1364,25 +1430,22 @@ void QgsMapToolEditMeshFrame::prepareSelection()
SelectedVertexData &vertexData = it.value();
int vertexIndex = it.key();
vertexData.selectedEdges.clear();
vertexData.meshFixedEdges.clear();
QgsMeshVertexCirculator circulator = mCurrentEditor->vertexCirculator( vertexIndex );
if ( !circulator.isValid() )
continue;
circulator.goBoundaryClockwise();
int firstface = circulator.currentFaceIndex();
do
{
int oppositeVertex = circulator.oppositeVertexClockwise();
if ( mSelectedVertices.contains( oppositeVertex ) )
vertexData.selectedEdges.append( {circulator.currentFaceIndex(), oppositeVertex} );
vertexData.borderEdges.append( {circulator.currentFaceIndex(), oppositeVertex} );
else
{
vertexData.meshFixedEdges.append( {circulator.currentFaceIndex(), oppositeVertex} );
borderSelectionVertices.insert( vertexIndex );
}
mConcernedFaceBySelection.insert( circulator.currentFaceIndex() );
}
@ -1393,11 +1456,9 @@ void QgsMapToolEditMeshFrame::prepareSelection()
circulator.turnClockwise();
int oppositeVertex = circulator.oppositeVertexCounterClockwise();
if ( mSelectedVertices.contains( oppositeVertex ) )
vertexData.selectedEdges.append( {-1, oppositeVertex} );
vertexData.borderEdges.append( {-1, oppositeVertex} );
else
vertexData.meshFixedEdges.append( {-1, oppositeVertex} );
borderSelectionVertices.insert( vertexIndex );
}
}
@ -1414,9 +1475,24 @@ void QgsMapToolEditMeshFrame::prepareSelection()
}
}
// here, we search for border edges that have associate face in the selection and remove it
for ( QMap<int, SelectedVertexData>::iterator it = mSelectedVertices.begin(); it != mSelectedVertices.end(); ++it )
{
SelectedVertexData &vertexData = it.value();
int i = 0;
while ( i < vertexData.borderEdges.count() )
{
int associateFace = vertexData.borderEdges.at( i ).first;
if ( associateFace == -1 || mSelectedFaces.contains( associateFace ) )
vertexData.borderEdges.removeAt( i );
else
i++;
}
}
if ( !mSelectedFaces.isEmpty() )
{
QList<int> faceList = mSelectedFaces.values();
const QList<int> faceList = qgis::setToList( mSelectedFaces );
QgsGeometry faceGeometrie( new QgsPolygon( new QgsLineString( nativeFaceGeometry( faceList.at( 0 ) ) ) ) );
if ( mSelectedFaces.count() == 1 )
{
@ -1428,10 +1504,10 @@ void QgsMapToolEditMeshFrame::prepareSelection()
geomEngine->prepareGeometry();
QVector<QgsGeometry> otherFaces( mSelectedFaces.count() );
for ( int i = 0; i < mSelectedFaces.count(); ++i )
for ( int i = 0; i < faceList.count(); ++i )
otherFaces[i] = QgsGeometry( new QgsPolygon( new QgsLineString( nativeFaceGeometry( faceList.at( i ) ) ) ) );
QString error;
QgsGeometry allFaces( geomEngine->combine( otherFaces, &error ) );
const QgsGeometry allFaces( geomEngine->combine( otherFaces, &error ) );
mSelectedFacesRubberband->setToGeometry( allFaces );
}
@ -1485,6 +1561,8 @@ void QgsMapToolEditMeshFrame::prepareSelection()
mActionSplitFaces->setText( tr( "Split %1 selected faces" ).arg( mSplittableFaceCount ) );
else
mActionSplitFaces->setText( tr( "Split current face" ) );
emit selectionChange( mCurrentLayer, mSelectedVertices.keys() );
}
void QgsMapToolEditMeshFrame::updateSelectecVerticesMarker()
@ -1493,7 +1571,6 @@ void QgsMapToolEditMeshFrame::updateSelectecVerticesMarker()
mSelectedVerticesMarker.clear();
for ( const int vertexIndex : mSelectedVertices.keys() )
{
mSelectedVertices.insert( vertexIndex, SelectedVertexData() );
QgsVertexMarker *marker = new QgsVertexMarker( canvas() );
marker->setIconType( QgsVertexMarker::ICON_CIRCLE );
marker->setIconSize( QgsGuiUtils::scaleIconSize( 10 ) );
@ -1505,6 +1582,22 @@ void QgsMapToolEditMeshFrame::updateSelectecVerticesMarker()
}
}
void QgsMapToolEditMeshFrame::setMovingRubberBandValidity( bool valid )
{
if ( valid )
{
mMovingFacesRubberband->setFillColor( QColor( 0, 200, 0, 100 ) );
mMovingFacesRubberband->setStrokeColor( QColor( 0, 200, 0 ) );
mMovingEdgesRubberband->setColor( QColor( 0, 200, 0 ) );
}
else
{
mMovingFacesRubberband->setFillColor( QColor( 200, 0, 0, 100 ) );
mMovingFacesRubberband->setStrokeColor( QColor( 200, 0, 0 ) );
mMovingEdgesRubberband->setColor( QColor( 200, 0, 0 ) );
}
}
void QgsMapToolEditMeshFrame::highlightCurrentHoveredFace( const QgsPointXY &mapPoint )
{
int faceIndex = -1;

View File

@ -31,6 +31,7 @@ class QgsRubberBand;
class QgsVertexMarker;
class QgsDoubleSpinBox;
class QgsSnapIndicator;
class QgsMeshTransformCoordinatesDockWidget;
class APP_EXPORT QgsZValueWidget : public QWidget
@ -76,6 +77,9 @@ class APP_EXPORT QgsMapToolEditMeshFrame : public QgsMapToolAdvancedDigitizing
bool populateContextMenuWithEvent( QMenu *menu, QgsMapMouseEvent *event ) override;
Flags flags() const override;
signals:
void selectionChange( QgsMeshLayer *meshLayer, const QList<int> verticesIndex );
protected:
void cadCanvasPressEvent( QgsMapMouseEvent *e ) override;
void cadCanvasReleaseEvent( QgsMapMouseEvent *e ) override;
@ -95,6 +99,8 @@ class APP_EXPORT QgsMapToolEditMeshFrame : public QgsMapToolAdvancedDigitizing
void removeFacesFromMesh();
void splitSelectedFaces();
void triggerTransformCoordinatesDockWidget( bool checked );
private:
enum State
@ -156,14 +162,14 @@ class APP_EXPORT QgsMapToolEditMeshFrame : public QgsMapToolAdvancedDigitizing
void prepareSelection();
void updateSelectecVerticesMarker();
bool testBorderMovingFace( const QgsMeshFace &borderMovingfaces, const QgsVector &translation ) const;
void setMovingRubberBandValidity( bool valid );
// members
struct SelectedVertexData
{
//Here edges are the indexes of the face where the following vertices (ccw) is the other extremity of the edge
QList<Edge> selectedEdges;
QList<Edge> meshFixedEdges;
QList<Edge> meshFixedEdges; // that have one extremity not on the selection
QList<Edge> borderEdges; // that are on the border of the selection
};
bool mIsInitialized = false;
@ -223,7 +229,6 @@ class APP_EXPORT QgsMapToolEditMeshFrame : public QgsMapToolAdvancedDigitizing
bool mCanMovingStart = false;
QgsRubberBand *mMovingEdgesRubberband = nullptr; //own by map canvas
QgsRubberBand *mMovingFacesRubberband = nullptr; //own by map canvas
QgsRubberBand *mMovingVerticesRubberband = nullptr; //own by map canvas
bool mIsMovingAllowed = false;
//! members for edge flip
@ -237,6 +242,8 @@ class APP_EXPORT QgsMapToolEditMeshFrame : public QgsMapToolAdvancedDigitizing
QgsZValueWidget *mZValueWidget = nullptr; //own by QgsUserInputWidget instance
QgsMeshTransformCoordinatesDockWidget *mTransformDockWidget = nullptr; //own by the application
QAction *mActionRemoveVerticesFillingHole = nullptr;
QAction *mActionRemoveVerticesWithoutFillingHole = nullptr;
QAction *mActionRemoveFaces = nullptr;
@ -248,6 +255,8 @@ class APP_EXPORT QgsMapToolEditMeshFrame : public QgsMapToolAdvancedDigitizing
QAction *mActionDigitizing = nullptr;
QAction *mActionSelectByPolygon = nullptr;
QAction *mActionTransformCoordinates = nullptr;
friend class TestQgsMapToolEditMesh;
};

View File

@ -0,0 +1,159 @@
/***************************************************************************
qgsmeshtransformcoordinatesdockwidget.cpp - QgsMeshTransformCoordinatesDockWidget
---------------------
begin : 26.8.2021
copyright : (C) 2021 by Vincent Cloarec
email : vcloarec 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 "qgsmeshtransformcoordinatesdockwidget.h"
#include "qgsgui.h"
#include "qgsexpressioncontextutils.h"
#include "qgsmesheditor.h"
#include "qgsmeshlayer.h"
#include "qgsmeshadvancedediting.h"
#include "qgsproject.h"
#include "qgsguiutils.h"
#include "qgshelp.h"
QgsMeshTransformCoordinatesDockWidget::QgsMeshTransformCoordinatesDockWidget( QWidget *parent ):
QgsDockWidget( parent )
{
setupUi( this );
QgsGui::enableAutoGeometryRestore( this );
setWindowTitle( tr( "Transform Mesh Vertices by Expression" ) );
mExpressionLineEdits << mExpressionEditX << mExpressionEditY << mExpressionEditZ;
mCheckBoxes << mCheckBoxX << mCheckBoxY << mCheckBoxZ;
Q_ASSERT( mExpressionLineEdits.count() == mCheckBoxes.count() );
for ( int i = 0; i < mExpressionLineEdits.count(); ++i )
{
mExpressionLineEdits.at( i )->registerExpressionContextGenerator( this );
mExpressionLineEdits.at( i )->setEnabled( mCheckBoxes.at( i )->isChecked() );
connect( mCheckBoxes.at( i ), &QCheckBox::toggled, mExpressionLineEdits.at( i ), &QWidget::setEnabled );
connect( mExpressionLineEdits.at( i ), &QgsExpressionLineEdit::expressionChanged, this, &QgsMeshTransformCoordinatesDockWidget::updateButton );
connect( mCheckBoxes.at( i ), &QCheckBox::toggled, this, &QgsMeshTransformCoordinatesDockWidget::updateButton );
}
connect( mButtonPreview, &QToolButton::clicked, this, &QgsMeshTransformCoordinatesDockWidget::calculate );
connect( mButtonApply, &QPushButton::clicked, this, &QgsMeshTransformCoordinatesDockWidget::apply );
}
QgsExpressionContext QgsMeshTransformCoordinatesDockWidget::createExpressionContext() const
{
return QgsExpressionContext( {QgsExpressionContextUtils::meshExpressionScope()} );
}
QgsMeshVertex QgsMeshTransformCoordinatesDockWidget::transformedVertex( int i )
{
if ( ! mInputLayer || !mIsCalculated )
return QgsMeshVertex();
return mTransformVertices.transformedVertex( mInputLayer, i );
}
bool QgsMeshTransformCoordinatesDockWidget::isResultValid() const
{
return mIsResultValid;
}
bool QgsMeshTransformCoordinatesDockWidget::isCalculated() const
{
return mIsCalculated;
}
void QgsMeshTransformCoordinatesDockWidget::setInput( QgsMeshLayer *layer, const QList<int> &vertexIndexes )
{
mInputLayer = layer;
mInputVertices = vertexIndexes;
mIsCalculated = false;
mIsResultValid = false;
if ( !mInputLayer )
mLabelInformation->setText( tr( "No active mesh layer" ) );
else
{
if ( !mInputLayer->isEditable() )
mLabelInformation->setText( tr( "Mesh layer \"%1\" not in edit mode" ).arg( mInputLayer->name() ) );
else
{
if ( mInputVertices.count() == 0 )
mLabelInformation->setText( tr( "No vertex selected for mesh \"%1\"" ).arg( mInputLayer->name() ) );
else if ( mInputVertices.count() == 1 )
mLabelInformation->setText( tr( "1 vertex of mesh layer \"%1\" to transform" ).arg( mInputLayer->name() ) );
else
mLabelInformation->setText( tr( "%1 vertices of mesh layer \"%2\" to transform" ).
arg( QString::number( mInputVertices.count() ), mInputLayer->name() ) );
}
}
updateButton();
emit calculationUpdated();
}
void QgsMeshTransformCoordinatesDockWidget::calculate()
{
if ( !mInputLayer || mInputVertices.isEmpty() )
return;
QgsTemporaryCursorOverride busyCursor( Qt::WaitCursor );
mTransformVertices.clear();
mTransformVertices.setInputVertices( mInputVertices );
mTransformVertices.setExpressions( mCheckBoxX->isChecked() ? mExpressionEditX->expression() : QString(),
mCheckBoxY->isChecked() ? mExpressionEditY->expression() : QString(),
mCheckBoxZ->isChecked() ? mExpressionEditZ->expression() : QString() );
QgsExpressionContext context;
context.appendScope( QgsExpressionContextUtils::projectScope( QgsProject::instance() ) );
mIsResultValid = mTransformVertices.calculate( mInputLayer );
mIsCalculated = true;
mButtonApply->setEnabled( mIsResultValid );
emit calculationUpdated();
}
void QgsMeshTransformCoordinatesDockWidget::updateButton()
{
mButtonApply->setEnabled( false );
bool isCalculable = mInputLayer && !mInputVertices.isEmpty();
if ( isCalculable )
{
isCalculable = false;
for ( const QCheckBox *cb : std::as_const( mCheckBoxes ) )
isCalculable |= cb->isChecked();
if ( isCalculable )
{
for ( int i = 0; i < mCheckBoxes.count(); ++i )
{
bool checked = mCheckBoxes.at( i )->isChecked();
isCalculable &= !checked || mExpressionLineEdits.at( i )->isValidExpression();
}
}
}
mButtonPreview->setEnabled( isCalculable );
}
void QgsMeshTransformCoordinatesDockWidget::apply()
{
emit aboutToBeApplied();
QgsTemporaryCursorOverride busyCursor( Qt::WaitCursor );
if ( mIsResultValid && mInputLayer && mInputLayer->meshEditor() )
mInputLayer->meshEditor()->advancedEdit( & mTransformVertices );
emit applied();
}

View File

@ -0,0 +1,82 @@
/***************************************************************************
qgsmeshtransformcoordinatesdockwidget.h - QgsMeshTransformCoordinatesDockWidget
---------------------
begin : 26.8.2021
copyright : (C) 2021 by Vincent Cloarec
email : vcloarec 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 QGSMESHTRANSFORMCOORDINATESDOCKWIDGET_H
#define QGSMESHTRANSFORMCOORDINATESDOCKWIDGET_H
#include "ui_qgsmeshtransformcoordinatesdockwidgetbase.h"
#include "qgsexpressioncontextgenerator.h"
#include "qgsmeshadvancedediting.h"
#include "qgisapp.h"
class QgsMeshLayer;
/**
* \brief A dock widget that is used to make some geometrical transformations of mesh vertices by expression
*
* \since QGIS 3.22
*/
class APP_EXPORT QgsMeshTransformCoordinatesDockWidget: public QgsDockWidget, public QgsExpressionContextGenerator, private Ui::QgsMeshTransformCoordinatesDockWidgetBase
{
Q_OBJECT
public:
//! Constructor
QgsMeshTransformCoordinatesDockWidget( QWidget *parent );
virtual QgsExpressionContext createExpressionContext() const override;
//! Returns the vertex with index \a vertexIndex after calculation
QgsMeshVertex transformedVertex( int vertexIndex );
//! Returns whether the result of transformation is a valid mesh
bool isResultValid() const;
//! Returns whether the calculation has been done
bool isCalculated() const;
signals:
//! Emitted when the calculation of the transform is done
void calculationUpdated();
//! Emitted just before the transform is applied
void aboutToBeApplied();
//! Emitted just after the transform is applied
void applied();
public slots:
//! Set the vertices indexes to transform \vertexIndexes for the mesh \a layer
void setInput( QgsMeshLayer *layer, const QList<int> &vertexIndexes );
private slots:
void calculate();
void updateButton();
void apply();
private:
QgsMeshTransformVerticesByExpression mTransformVertices;
QgsMeshLayer *mInputLayer;
QList<int> mInputVertices;
bool mIsCalculated = false;
bool mIsResultValid = false;
QList<QgsExpressionLineEdit *> mExpressionLineEdits;
QList<QCheckBox *> mCheckBoxes;
};
#endif // QGSMESHTRANSFORMCOORDINATESDOCKWIDGET_H

View File

@ -1019,6 +1019,82 @@ class CurrentVertexZValueExpressionFunction: public QgsScopedExpressionFunction
}
};
class CurrentVertexXValueExpressionFunction: public QgsScopedExpressionFunction
{
public:
CurrentVertexXValueExpressionFunction():
QgsScopedExpressionFunction( "$vertex_x",
0,
QStringLiteral( "Meshes" ) )
{}
QgsScopedExpressionFunction *clone() const override {return new CurrentVertexXValueExpressionFunction();}
QVariant func( const QVariantList &, const QgsExpressionContext *context, QgsExpression *, const QgsExpressionNodeFunction * ) override
{
if ( !context )
return QVariant();
if ( !context->hasVariable( QStringLiteral( "_mesh_vertex_index" ) ) || !context->hasVariable( QStringLiteral( "_mesh_layer" ) ) )
return QVariant();
int vertexIndex = context->variable( QStringLiteral( "_mesh_vertex_index" ) ).toInt();
QgsMeshLayer *layer = qobject_cast<QgsMeshLayer *>( qvariant_cast<QgsMapLayer *>( context->variable( QStringLiteral( "_mesh_layer" ) ) ) );
if ( !layer || !layer->nativeMesh() || layer->nativeMesh()->vertexCount() <= vertexIndex )
return QVariant();
const QgsMeshVertex &vertex = layer->nativeMesh()->vertex( vertexIndex );
if ( !vertex.isEmpty() )
return vertex.x();
else
return QVariant();
}
bool isStatic( const QgsExpressionNodeFunction *, QgsExpression *, const QgsExpressionContext * ) const override
{
return false;
}
};
class CurrentVertexYValueExpressionFunction: public QgsScopedExpressionFunction
{
public:
CurrentVertexYValueExpressionFunction():
QgsScopedExpressionFunction( "$vertex_y",
0,
QStringLiteral( "Meshes" ) )
{}
QgsScopedExpressionFunction *clone() const override {return new CurrentVertexYValueExpressionFunction();}
QVariant func( const QVariantList &, const QgsExpressionContext *context, QgsExpression *, const QgsExpressionNodeFunction * ) override
{
if ( !context )
return QVariant();
if ( !context->hasVariable( QStringLiteral( "_mesh_vertex_index" ) ) || !context->hasVariable( QStringLiteral( "_mesh_layer" ) ) )
return QVariant();
int vertexIndex = context->variable( QStringLiteral( "_mesh_vertex_index" ) ).toInt();
QgsMeshLayer *layer = qobject_cast<QgsMeshLayer *>( qvariant_cast<QgsMapLayer *>( context->variable( QStringLiteral( "_mesh_layer" ) ) ) );
if ( !layer || !layer->nativeMesh() || layer->nativeMesh()->vertexCount() <= vertexIndex )
return QVariant();
const QgsMeshVertex &vertex = layer->nativeMesh()->vertex( vertexIndex );
if ( !vertex.isEmpty() )
return vertex.y();
else
return QVariant();
}
bool isStatic( const QgsExpressionNodeFunction *, QgsExpression *, const QgsExpressionContext * ) const override
{
return false;
}
};
class CurrentVertexExpressionFunction: public QgsScopedExpressionFunction
{
public:
@ -1060,10 +1136,14 @@ class CurrentVertexExpressionFunction: public QgsScopedExpressionFunction
QgsExpressionContextScope *QgsExpressionContextUtils::meshExpressionScope()
{
QgsExpression::registerFunction( new CurrentVertexExpressionFunction, true );
QgsExpression::registerFunction( new CurrentVertexXValueExpressionFunction, true );
QgsExpression::registerFunction( new CurrentVertexYValueExpressionFunction, true );
QgsExpression::registerFunction( new CurrentVertexZValueExpressionFunction, true );
std::unique_ptr<QgsExpressionContextScope> scope = std::make_unique<QgsExpressionContextScope>();
scope->addFunction( "$vertex_as_point", new CurrentVertexExpressionFunction );
scope->addFunction( "$vertex_x", new CurrentVertexXValueExpressionFunction );
scope->addFunction( "$vertex_y", new CurrentVertexYValueExpressionFunction );
scope->addFunction( "$vertex_z", new CurrentVertexZValueExpressionFunction );
return scope.release();

View File

@ -19,6 +19,10 @@
#include "qgsmesheditor.h"
#include "poly2tri.h"
#include "qgsmeshlayer.h"
#include "qgsexpression.h"
#include "qgsexpressioncontextutils.h"
QgsMeshAdvancedEditing::QgsMeshAdvancedEditing() = default;
QgsMeshAdvancedEditing::~QgsMeshAdvancedEditing() = default;
@ -591,3 +595,181 @@ bool QgsMeshEditRefineFaces::createNewBorderFaces( QgsMeshEditor *meshEditor,
return true;
}
bool QgsMeshTransformVerticesByExpression::calculate( QgsMeshLayer *layer )
{
if ( !layer || !layer->meshEditor() || !layer->nativeMesh() )
return false;
if ( mInputVertices.isEmpty() )
return false;
const QgsMesh mesh = *layer->nativeMesh();
QSet<int> concernedFaces;
mChangingVertexMap = QHash<int, int>();
std::unique_ptr<QgsExpressionContextScope> expScope( QgsExpressionContextUtils::meshExpressionScope() );
QgsExpressionContext context;
context.appendScope( expScope.release() );
context.lastScope()->setVariable( QStringLiteral( "_mesh_layer" ), QVariant::fromValue( layer ) );
QVector<QgsMeshVertex> newVertices;
newVertices.reserve( mInputVertices.count() );
int inputCount = mInputVertices.count();
mChangeCoordinateVerticesIndexes = mInputVertices;
bool calcX = !mExpressionX.isEmpty();
bool calcY = !mExpressionY.isEmpty();
bool calcZ = !mExpressionZ.isEmpty();
QgsExpression expressionX;
if ( calcX )
{
expressionX = QgsExpression( mExpressionX );
expressionX.prepare( &context );
}
QgsExpression expressionY;
if ( calcY )
{
expressionY = QgsExpression( mExpressionY );
expressionY.prepare( &context );
}
if ( calcX || calcY )
{
mNewXYValues.reserve( inputCount );
mOldXYValues.reserve( inputCount );
}
QgsExpression expressionZ;
if ( calcZ )
{
expressionZ = QgsExpression( mExpressionZ );
expressionZ.prepare( &context );
mNewZValues.reserve( inputCount );
mOldZValues.reserve( inputCount );
}
for ( int i = 0; i < mInputVertices.count(); ++i )
{
const int vertexIndex = mInputVertices.at( i );
context.lastScope()->setVariable( QStringLiteral( "_mesh_vertex_index" ), vertexIndex, false );
mChangingVertexMap[vertexIndex] = i;
const QVariant xvar = expressionX.evaluate( &context );
const QVariant yvar = expressionY.evaluate( &context );
const QVariant zvar = expressionZ.evaluate( &context );
const QgsMeshVertex &vert = mesh.vertex( vertexIndex );
if ( calcX || calcY )
{
mOldXYValues.append( QgsPointXY( vert ) );
mNewXYValues.append( QgsPointXY( vert ) );
const QList<int> facesAround = layer->meshEditor()->topologicalMesh().facesAroundVertex( vertexIndex );
concernedFaces.unite( qgis::listToSet( facesAround ) );
}
bool ok = false;
if ( calcX )
{
if ( xvar.isValid() )
{
double x = xvar.toDouble( &ok );
if ( ok )
{
mNewXYValues.last().setX( x );
}
else
return false;
}
else
{
return false;
}
}
if ( calcY )
{
if ( yvar.isValid() )
{
double y = yvar.toDouble( &ok );
if ( ok )
{
mNewXYValues.last().setY( y );
}
else
return false;
}
else
return false;
}
if ( calcZ )
{
if ( zvar.isValid() )
{
double z = zvar.toDouble( &ok );
if ( ok )
{
mNewZValues.append( z );
mOldZValues.append( vert.z() );
}
else
return false;
}
else
return false;
}
}
auto transformFunction = [this, layer ]( int vi )-> const QgsMeshVertex
{
return transformedVertex( layer, vi );
};
mNativeFacesIndexesGeometryChanged = qgis::setToList( concernedFaces );
return layer->meshEditor()->canBeTransformed( mNativeFacesIndexesGeometryChanged, transformFunction );
}
void QgsMeshTransformVerticesByExpression::setExpressions( const QString &expressionX, const QString &expressionY, const QString &expressionZ )
{
mExpressionX = expressionX;
mExpressionY = expressionY;
mExpressionZ = expressionZ;
mChangingVertexMap.clear();
}
QgsTopologicalMesh::Changes QgsMeshTransformVerticesByExpression::apply( QgsMeshEditor *meshEditor )
{
meshEditor->topologicalMesh().applyChanges( *this );
return *this;
}
QgsMeshVertex QgsMeshTransformVerticesByExpression::transformedVertex( QgsMeshLayer *layer, int vertexIndex ) const
{
int pos = mChangingVertexMap.value( vertexIndex, -1 );
if ( pos > -1 )
{
QgsPointXY pointXY;
double z;
if ( mNewXYValues.isEmpty() )
pointXY = layer->nativeMesh()->vertex( vertexIndex );
else
pointXY = mNewXYValues.at( pos );
if ( mNewZValues.isEmpty() )
z = layer->nativeMesh()->vertex( vertexIndex ).z();
else
z = mNewZValues.at( pos );
return QgsMeshVertex( pointXY.x(), pointXY.y(), z );
}
else
return layer->nativeMesh()->vertex( vertexIndex );
}

View File

@ -23,6 +23,7 @@
class QgsMeshEditor;
class QgsProcessingFeedback;
class QgsExpressionContext;
/**
* \ingroup core
@ -124,4 +125,67 @@ class CORE_EXPORT QgsMeshEditRefineFaces : public QgsMeshAdvancedEditing
};
/**
* \ingroup core
*
* \brief Class that can transform vertices of a mesh by expression
*
* Each coordinates are associated with an expression that can be defined with function
* returning the current coordinates (see setExpressions()):
*
* - $vertex_x
* - $vertex_y
* - $vertex_z
*
* Example:
* Transposing a mesh and translate following axe X with a distance of 50 and increase the level of the mesh
* with an height of 80 when previous X coordinate is under 100 and de crease the level of 150 when X is under 100:
*
* expressionX: "$vertex_y + 50"
* expressionY: "$vertex_x"
* expressionZ: "if( $vertex_x <= 100 , $vertex_z + 80 , $vertex_z - 150)"
*
* \since QGIS 3.22
*/
class CORE_EXPORT QgsMeshTransformVerticesByExpression : public QgsMeshAdvancedEditing
{
public:
//! Constructor
QgsMeshTransformVerticesByExpression() = default;
/**
* Sets the expressions for the coordinates transformation.
*
* \note Expressions are optional for each coordinate, the coordinate will not be transformed if the string is void.
*/
void setExpressions( const QString &expressionX, const QString &expressionY, const QString &expressionZ );
/**
* Calculates the transformed vertices of the mesh \a layer, returns FALSE if this leads to topological or geometrical errors.
* The mesh layer must be in edit mode.
*
* \note this method not apply new vertices to the mesh layer but only store the calculated transformation
* that can be apply later with QgsMeshEditor::advancedEdit()
*/
bool calculate( QgsMeshLayer *layer );
/**
* Returns the transformed vertex from its index \a vertexIndex for the mesh \a layer
*
* If \a layer is not the same than the one used to make the calculation, this will create an undefined behavior
*/
QgsMeshVertex transformedVertex( QgsMeshLayer *layer, int vertexIndex ) const;
private:
QString mExpressionX;
QString mExpressionY;
QString mExpressionZ;
QHash<int, int> mChangingVertexMap;
QgsTopologicalMesh::Changes apply( QgsMeshEditor *meshEditor ) override;
friend class TestQgsMeshEditor;
};
#endif // QGSMESHADVANCEDEDITING_H

View File

@ -257,8 +257,6 @@ void QgsMeshEditor::applyEditOnTriangularMesh( QgsMeshEditor::Edit &edit, const
void QgsMeshEditor::applyAdvancedEdit( QgsMeshEditor::Edit &edit, QgsMeshAdvancedEditing *editing )
{
applyEditOnTriangularMesh( edit, editing->apply( this ) );
emit meshEdited();
}
bool QgsMeshEditor::checkConsistency() const
@ -358,7 +356,7 @@ QVector<QgsMeshFace> QgsMeshEditor::prepareFaces( const QVector<QgsMeshFace> &fa
break;
}
error = mTopologicalMesh.counterClockWiseFaces( face, mMesh );
error = mTopologicalMesh.counterClockwiseFaces( face, mMesh );
if ( error.errorType != Qgis::MeshEditingErrorType::NoError )
break;
}
@ -464,6 +462,96 @@ void QgsMeshEditor::changeZValues( const QList<int> &verticesIndexes, const QLis
mUndoStack->push( new QgsMeshLayerUndoCommandChangeZValue( this, verticesIndexes, newZValues ) );
}
bool QgsMeshEditor::canBeTransformed( const QList<int> &transformedFaces, const std::function<const QgsMeshVertex( int )> &transformFunction ) const
{
for ( const int faceIndex : transformedFaces )
{
const QgsMeshFace &face = mMesh->face( faceIndex );
int faceSize = face.count();
QVector<QgsPointXY> pointsInTriangularMeshCoordinate( faceSize );
QVector<QgsPointXY> points( faceSize );
for ( int i = 0; i < faceSize; ++i )
{
int ip0 = face[i];
int ip1 = face[( i + 1 ) % faceSize];
int ip2 = face[( i + 2 ) % faceSize];
QgsMeshVertex p0 = transformFunction( ip0 );
QgsMeshVertex p1 = transformFunction( ip1 );
QgsMeshVertex p2 = transformFunction( ip2 );
double ux = p0.x() - p1.x();
double uy = p0.y() - p1.y();
double vx = p2.x() - p1.x();
double vy = p2.y() - p1.y();
double crossProduct = ux * vy - uy * vx;
if ( crossProduct >= 0 ) //if cross product>0, we have two edges clockwise
return false;
pointsInTriangularMeshCoordinate[i] = mTriangularMesh->nativeToTriangularCoordinates( p0 );
points[i] = p0;
}
const QgsGeometry &deformedFace = QgsGeometry::fromPolygonXY( {points} );
// now test if the deformed face contain something else (search canditate <ith the
QList<int> otherFaceIndexes =
mTriangularMesh->nativeFaceIndexForRectangle( QgsGeometry::fromPolygonXY( {pointsInTriangularMeshCoordinate} ).boundingBox() );
for ( const int otherFaceIndex : otherFaceIndexes )
{
const QgsMeshFace &otherFace = mMesh->face( otherFaceIndex );
int existingFaceSize = otherFace.count();
bool shareVertex = false;
for ( int i = 0; i < existingFaceSize; ++i )
{
if ( face.contains( otherFace.at( i ) ) )
{
shareVertex = true;
break;
}
}
if ( shareVertex )
{
//only test the edge that not contain a shared vertex
for ( int i = 0; i < existingFaceSize; ++i )
{
int index1 = otherFace.at( i );
int index2 = otherFace.at( ( i + 1 ) % existingFaceSize );
if ( ! face.contains( index1 ) && !face.contains( index2 ) )
{
const QgsPointXY &v1 = transformFunction( index1 );
const QgsPointXY &v2 = transformFunction( index2 );
QgsGeometry edgeGeom = QgsGeometry::fromPolylineXY( { v1, v2} );
if ( deformedFace.intersects( edgeGeom ) )
return false;
}
}
}
else
{
QVector<QgsPointXY> otherPoints( existingFaceSize );
for ( int i = 0; i < existingFaceSize; ++i )
otherPoints[i] = transformFunction( otherFace.at( i ) );
const QgsGeometry existingFaceGeom = QgsGeometry::fromPolygonXY( {otherPoints } );
if ( deformedFace.intersects( existingFaceGeom ) )
return false;
}
}
//finish with free vertices...
const QList<int> freeVerticesIndex = freeVerticesIndexes();
for ( const int vertexIndex : freeVerticesIndex )
{
const QgsPointXY &mapPoint = transformFunction( vertexIndex ); //free vertices can be transformed
if ( deformedFace.contains( &mapPoint ) )
return false;
}
}
return true;
}
void QgsMeshEditor::changeXYValues( const QList<int> &verticesIndexes, const QList<QgsPointXY> &newValues )
{
// TODO : implement a check if it is possible to change the (x,y) values. For now, this check is made in the APP part
@ -626,7 +714,7 @@ bool QgsMeshEditor::isModified() const
return false;
}
QList<int> QgsMeshEditor::freeVerticesIndexes()
QList<int> QgsMeshEditor::freeVerticesIndexes() const
{
return mTopologicalMesh.freeVerticesIndexes();
}

View File

@ -152,6 +152,15 @@ class CORE_EXPORT QgsMeshEditor : public QObject
*/
void changeZValues( const QList<int> &verticesIndexes, const QList<double> &newValues );
/**
* Returns TRUE if faces with index in \a transformedFaces can be transformed without obtaining topologic or geometrical errors
* condidering the transform function \a transformFunction
*
* The transform function takes a vertex index in parameter and return a QgsMeshVertex object with transformed coordinates.
* This transformation is done in layer coordinates
*/
bool canBeTransformed( const QList<int> &transformedFaces, const std::function<const QgsMeshVertex( int )> &transformFunction ) const; SIP_SKIP
/**
* Changes the (X,Y) coordinates values of the vertices with indexes in \a vertices indexes with the values in \a newValues.
* The caller has the responsibility to check if changing the vertices coordinates does not lead to topological errors
@ -175,7 +184,7 @@ class CORE_EXPORT QgsMeshEditor : public QObject
//----------- access element methods
//! Returns all the free vertices indexes
QList<int> freeVerticesIndexes();
QList<int> freeVerticesIndexes() const;
//! Returns whether the vertex with index \a vertexIndex is on a boundary
bool isVertexOnBoundary( int vertexIndex ) const;

View File

@ -585,7 +585,7 @@ QList<int> QgsTopologicalMesh::freeVerticesIndexes() const
#endif
}
QgsMeshEditingError QgsTopologicalMesh::counterClockWiseFaces( QgsMeshFace &face, QgsMesh *mesh )
QgsMeshEditingError QgsTopologicalMesh::counterClockwiseFaces( QgsMeshFace &face, QgsMesh *mesh )
{
// First check if the face is convex and put it counter clockwise
// If the index are not well ordered (edges intersect), invalid face --> return false
@ -1227,7 +1227,7 @@ QgsTopologicalMesh QgsTopologicalMesh::createTopologicalMesh( QgsMesh *mesh, int
break;
}
error = counterClockWiseFaces( mesh->faces[i], mesh );
error = counterClockwiseFaces( mesh->faces[i], mesh );
if ( error.errorType != Qgis::MeshEditingErrorType::NoError )
{
if ( error.errorType == Qgis::MeshEditingErrorType::InvalidFace || error.errorType == Qgis::MeshEditingErrorType::FlatFace )

View File

@ -285,8 +285,8 @@ class CORE_EXPORT QgsTopologicalMesh
//! Reverses the changes
void reverseChanges( const Changes &changes );
//! Checks the topology of the face and sets it counter clock wise if necessary
static QgsMeshEditingError counterClockWiseFaces( QgsMeshFace &face, QgsMesh *mesh );
//! Checks the topology of the face and sets it counter clockwise if necessary
static QgsMeshEditingError counterClockwiseFaces( QgsMeshFace &face, QgsMesh *mesh );
/**
* Reindexes faces and vertices, after this operation, the topological

View File

@ -142,7 +142,8 @@ QString QgsExpressionLineEdit::expression() const
bool QgsExpressionLineEdit::isValidExpression( QString *expressionError ) const
{
QString temp;
return QgsExpression::checkExpression( expression(), &mExpressionContext, expressionError ? *expressionError : temp );
const QgsExpressionContext context = mExpressionContextGenerator ? mExpressionContextGenerator->createExpressionContext() : mExpressionContext;
return QgsExpression::checkExpression( expression(), &context, expressionError ? *expressionError : temp );
}
void QgsExpressionLineEdit::registerExpressionContextGenerator( const QgsExpressionContextGenerator *generator )
@ -230,6 +231,8 @@ void QgsExpressionLineEdit::updateLineEditStyle( const QString &expression )
bool QgsExpressionLineEdit::isExpressionValid( const QString &expressionStr )
{
QgsExpression expression( expressionStr );
const QgsExpressionContext context = mExpressionContextGenerator ? mExpressionContextGenerator->createExpressionContext() : mExpressionContext;
expression.prepare( &mExpressionContext );
return !expression.hasParserError();
}

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QgsMeshTransformCoordinatesDockWidgetBase</class>
<widget class="QgsDockWidget" name="QgsMeshTransformCoordinatesDockWidgetBase">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>256</width>
<height>174</height>
</rect>
</property>
<property name="floating">
<bool>false</bool>
</property>
<property name="windowTitle">
<string notr="true">DockWidget</string>
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="1">
<widget class="QgsExpressionLineEdit" name="mExpressionEditZ" native="true"/>
</item>
<item row="2" column="1">
<widget class="QgsExpressionLineEdit" name="mExpressionEditY" native="true"/>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="mCheckBoxY">
<property name="text">
<string>Y coordinate</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="mCheckBoxX">
<property name="text">
<string>X coordinate</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QgsExpressionLineEdit" name="mExpressionEditX" native="true"/>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="mCheckBoxZ">
<property name="text">
<string>Z value</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QToolButton" name="mButtonPreview">
<property name="text">
<string>Preview transform</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="mButtonApply">
<property name="text">
<string>Apply transform</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="mLabelInformation">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>TextLabel</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>QgsDockWidget</class>
<extends>QDockWidget</extends>
<header>qgsdockwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsExpressionLineEdit</class>
<extends>QWidget</extends>
<header>qgsexpressionlineedit.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -57,6 +57,8 @@ class TestQgsMeshEditor : public QObject
void refineMesh();
void transformByExpression();
void particularCases();
};
@ -1679,5 +1681,132 @@ void TestQgsMeshEditor::refineMesh()
}
}
void TestQgsMeshEditor::transformByExpression()
{
std::unique_ptr<QgsMeshLayer> layer = std::make_unique<QgsMeshLayer>( mDataDir + "/quad_flower_to_edit.2dm", "mesh", "mdal" );
const QgsCoordinateTransform transform;
layer->startFrameEditing( transform );
QgsMeshTransformVerticesByExpression transformVertex;
transformVertex.setExpressions( QStringLiteral( "$vertex_x + 50" ), QStringLiteral( "$vertex_y - 50" ), QStringLiteral( "$vertex_z + 100" ) );
// no input set
QVERIFY( !transformVertex.calculate( layer.get() ) );
transformVertex.setInputVertices( {0, 1, 3, 4} );
QVERIFY( transformVertex.calculate( layer.get() ) );
QCOMPARE( transformVertex.mChangeCoordinateVerticesIndexes, QList<int>( {0, 1, 3, 4} ) );
QVERIFY( transformVertex.mOldXYValues.at( 0 ).compare( QgsPointXY( 1000, 2000 ), 0.1 ) );
QVERIFY( transformVertex.mOldXYValues.at( 1 ).compare( QgsPointXY( 2000, 2000 ), 0.1 ) );
QVERIFY( transformVertex.mOldXYValues.at( 2 ).compare( QgsPointXY( 2000, 3000 ), 0.1 ) );
QVERIFY( transformVertex.mOldXYValues.at( 3 ).compare( QgsPointXY( 1000, 3000 ), 0.1 ) );
QVERIFY( transformVertex.mNewXYValues.at( 0 ).compare( QgsPointXY( 1050, 1950 ), 0.1 ) );
QVERIFY( transformVertex.mNewXYValues.at( 1 ).compare( QgsPointXY( 2050, 1950 ), 0.1 ) );
QVERIFY( transformVertex.mNewXYValues.at( 2 ).compare( QgsPointXY( 2050, 2950 ), 0.1 ) );
QVERIFY( transformVertex.mNewXYValues.at( 3 ).compare( QgsPointXY( 1050, 2950 ), 0.1 ) );
QCOMPARE( transformVertex.mNewZValues.at( 0 ), 300 );
QCOMPARE( transformVertex.mNewZValues.at( 1 ), 300 );
QCOMPARE( transformVertex.mNewZValues.at( 2 ), 300 );
QCOMPARE( transformVertex.mNewZValues.at( 3 ), 300 );
layer->meshEditor()->advancedEdit( &transformVertex );
QgsMesh &mesh = *layer->nativeMesh();
QVERIFY( QgsPoint( 1050, 1950, 300 ).compareTo( &mesh.vertices.at( 0 ) ) == 0 );
QVERIFY( QgsPoint( 2050, 1950, 300 ).compareTo( &mesh.vertices.at( 1 ) ) == 0 );
QVERIFY( QgsPoint( 2500, 2500, 800 ).compareTo( &mesh.vertices.at( 2 ) ) == 0 );
QVERIFY( QgsPoint( 2050, 2950, 300 ).compareTo( &mesh.vertices.at( 3 ) ) == 0 );
QVERIFY( QgsPoint( 1050, 2950, 300 ).compareTo( &mesh.vertices.at( 4 ) ) == 0 );
QVERIFY( QgsPoint( 500, 2500, 800 ).compareTo( &mesh.vertices.at( 5 ) ) == 0 );
QVERIFY( QgsPoint( 1500, 1500, 800 ).compareTo( &mesh.vertices.at( 6 ) ) == 0 );
QVERIFY( QgsPoint( 1500, 3500, 800 ).compareTo( &mesh.vertices.at( 7 ) ) == 0 );
layer->undoStack()->undo();
mesh = *layer->nativeMesh();
QVERIFY( QgsPoint( 1000, 2000, 200 ).compareTo( &mesh.vertices.at( 0 ) ) == 0 );
QVERIFY( QgsPoint( 2000, 2000, 200 ).compareTo( &mesh.vertices.at( 1 ) ) == 0 );
QVERIFY( QgsPoint( 2500, 2500, 800 ).compareTo( &mesh.vertices.at( 2 ) ) == 0 );
QVERIFY( QgsPoint( 2000, 3000, 200 ).compareTo( &mesh.vertices.at( 3 ) ) == 0 );
QVERIFY( QgsPoint( 1000, 3000, 200 ).compareTo( &mesh.vertices.at( 4 ) ) == 0 );
QVERIFY( QgsPoint( 500, 2500, 800 ).compareTo( &mesh.vertices.at( 5 ) ) == 0 );
QVERIFY( QgsPoint( 1500, 1500, 800 ).compareTo( &mesh.vertices.at( 6 ) ) == 0 );
QVERIFY( QgsPoint( 1500, 3500, 800 ).compareTo( &mesh.vertices.at( 7 ) ) == 0 );
layer->undoStack()->redo();
mesh = *layer->nativeMesh();
QVERIFY( QgsPoint( 1050, 1950, 300 ).compareTo( &mesh.vertices.at( 0 ) ) == 0 );
QVERIFY( QgsPoint( 2050, 1950, 300 ).compareTo( &mesh.vertices.at( 1 ) ) == 0 );
QVERIFY( QgsPoint( 2500, 2500, 800 ).compareTo( &mesh.vertices.at( 2 ) ) == 0 );
QVERIFY( QgsPoint( 2050, 2950, 300 ).compareTo( &mesh.vertices.at( 3 ) ) == 0 );
QVERIFY( QgsPoint( 1050, 2950, 300 ).compareTo( &mesh.vertices.at( 4 ) ) == 0 );
QVERIFY( QgsPoint( 500, 2500, 800 ).compareTo( &mesh.vertices.at( 5 ) ) == 0 );
QVERIFY( QgsPoint( 1500, 1500, 800 ).compareTo( &mesh.vertices.at( 6 ) ) == 0 );
QVERIFY( QgsPoint( 1500, 3500, 800 ).compareTo( &mesh.vertices.at( 7 ) ) == 0 );
layer->undoStack()->undo();
// leads to an invalid mesh
transformVertex.clear();
transformVertex.setInputVertices( {1, 3} );
transformVertex.setExpressions( QStringLiteral( "$vertex_x -1500" ), QStringLiteral( "$vertex_y - 1500" ), QString() );
QVERIFY( !transformVertex.calculate( layer.get() ) );
// transforme with intersecting existing faces
transformVertex.clear();
transformVertex.setInputVertices( {2, 3, 7} );
transformVertex.setExpressions( QStringLiteral( "$vertex_x+700" ), QStringLiteral( "$vertex_y + 700" ), QString() );
QVERIFY( transformVertex.calculate( layer.get() ) );
//add a other face that will intersects transformed ones
layer->meshEditor()->addVertices(
{
{2000, 3500, 0}, // 8
{2500, 3500, 10}, // 9
{2500, 4000, 20}} // 10
, 1 );
QVERIFY( !transformVertex.calculate( layer.get() ) );
layer->meshEditor()->addFace( {8, 9, 10} );
QVERIFY( !transformVertex.calculate( layer.get() ) );
// undo adding vertices and face
layer->undoStack()->undo();
layer->undoStack()->undo();
QVERIFY( transformVertex.calculate( layer.get() ) );
// composed expression
transformVertex.clear();
transformVertex.setInputVertices( {0, 1, 2, 3, 4, 5, 6, 7} );
transformVertex.setExpressions( QStringLiteral( "$vertex_y + 50" ),
QStringLiteral( "-$vertex_x" ),
QStringLiteral( "if( $vertex_x <= 1500 , $vertex_z + 80 , $vertex_z - 150)" ) );
QVERIFY( transformVertex.calculate( layer.get() ) );
layer->meshEditor()->advancedEdit( &transformVertex );
mesh = *layer->nativeMesh();
QVERIFY( QgsPoint( 2050, -1000, 280 ).compareTo( &mesh.vertices.at( 0 ) ) == 0 );
QVERIFY( QgsPoint( 2050, -2000, 50 ).compareTo( &mesh.vertices.at( 1 ) ) == 0 );
QVERIFY( QgsPoint( 2550, -2500, 650 ).compareTo( &mesh.vertices.at( 2 ) ) == 0 );
QVERIFY( QgsPoint( 3050, -2000, 50 ).compareTo( &mesh.vertices.at( 3 ) ) == 0 );
QVERIFY( QgsPoint( 3050, -1000, 280 ).compareTo( &mesh.vertices.at( 4 ) ) == 0 );
QVERIFY( QgsPoint( 2550, -500, 880 ).compareTo( &mesh.vertices.at( 5 ) ) == 0 );
QVERIFY( QgsPoint( 1550, -1500, 880 ).compareTo( &mesh.vertices.at( 6 ) ) == 0 );
QVERIFY( QgsPoint( 3550, -1500, 880 ).compareTo( &mesh.vertices.at( 7 ) ) == 0 );
}
QGSTEST_MAIN( TestQgsMeshEditor )
#include "testqgsmesheditor.moc"