Add ability for QgsLayoutSnapper to snap to grid

This commit is contained in:
Nyall Dawson 2017-07-24 18:13:55 +10:00
parent 361dd312bc
commit 5be237fdd9
5 changed files with 281 additions and 7 deletions

View File

@ -7,6 +7,7 @@
************************************************************************/
class QgsLayoutSnapper
{
%Docstring
@ -27,7 +28,23 @@ class QgsLayoutSnapper
GridCrosses
};
QgsLayoutSnapper();
QgsLayoutSnapper( QgsLayout *layout );
%Docstring
Constructor for QgsLayoutSnapper, attached to the specified ``layout``.
%End
void setSnapTolerance( const int snapTolerance );
%Docstring
Sets the snap ``tolerance`` (in pixels) to use when snapping.
.. seealso:: snapTolerance()
%End
int snapTolerance() const;
%Docstring
Returns the snap tolerance (in pixels) to use when snapping.
.. seealso:: setSnapTolerance()
:rtype: int
%End
void setGridResolution( const QgsLayoutMeasurement &resolution );
%Docstring
@ -89,6 +106,45 @@ class QgsLayoutSnapper
:rtype: GridStyle
%End
bool snapToGrid() const;
%Docstring
Returns true if snapping to grid is enabled.
.. seealso:: setSnapToGrid()
:rtype: bool
%End
void setSnapToGrid( bool enabled );
%Docstring
Sets whether snapping to grid is ``enabled``.
.. seealso:: snapToGrid()
%End
QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped /Out/ ) const;
%Docstring
Snaps a layout coordinate ``point``. If ``point`` was snapped, ``snapped`` will be set to true.
The ``scaleFactor`` argument should be set to the transformation from
scalar transform from layout coordinates to pixels, i.e. the
graphics view transform().m11() value.
This method considers snapping to the grid, snap lines, etc.
:rtype: QPointF
%End
QPointF snapPointToGrid( QPointF point, double scaleFactor, bool &snapped /Out/ ) const;
%Docstring
Snaps a layout coordinate ``point`` to the grid. If ``point``
was snapped, ``snapped`` will be set to true.
The ``scaleFactor`` argument should be set to the transformation from
scalar transform from layout coordinates to pixels, i.e. the
graphics view transform().m11() value.
If snapToGrid() is disabled, this method will return the point
unchanged.
:rtype: QPointF
%End
};
/************************************************************************

View File

@ -20,6 +20,7 @@
QgsLayout::QgsLayout( QgsProject *project )
: QGraphicsScene()
, mProject( project )
, mSnapper( QgsLayoutSnapper( this ) )
, mPageCollection( new QgsLayoutPageCollection( this ) )
{
// just to make sure - this should be the default, but maybe it'll change in some future Qt version...

View File

@ -15,10 +15,76 @@
***************************************************************************/
#include "qgslayoutsnapper.h"
#include "qgslayout.h"
QgsLayoutSnapper::QgsLayoutSnapper()
: mGridResolution( QgsLayoutMeasurement( 10 ) )
QgsLayoutSnapper::QgsLayoutSnapper( QgsLayout *layout )
: mLayout( layout )
, mGridResolution( QgsLayoutMeasurement( 10 ) )
{
mGridPen = QPen( QColor( 190, 190, 190, 100 ), 0 );
mGridPen.setCosmetic( true );
}
QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &snapped ) const
{
snapped = false;
// highest priority - grid
bool snappedToGrid = false;
QPointF res = snapPointToGrid( point, scaleFactor, snappedToGrid );
if ( snappedToGrid )
{
snapped = true;
return res;
}
return point;
}
QPointF QgsLayoutSnapper::snapPointToGrid( QPointF point, double scaleFactor, bool &snapped ) const
{
snapped = false;
if ( !mLayout || !mSnapToGrid || mGridResolution.length() <= 0 )
{
return point;
}
//calculate y offset to current page
QPointF pagePoint = mLayout->pageCollection()->positionOnPage( point );
double yPage = pagePoint.y(); //y-coordinate relative to current page
double yAtTopOfPage = mLayout->pageCollection()->page( mLayout->pageCollection()->pageNumberForPoint( point ) )->pos().y();
//snap x coordinate
double gridRes = mLayout->convertToLayoutUnits( mGridResolution );
QPointF gridOffset = mLayout->convertToLayoutUnits( mGridOffset );
int xRatio = static_cast< int >( ( point.x() - gridOffset.x() ) / gridRes + 0.5 ); //NOLINT
int yRatio = static_cast< int >( ( yPage - gridOffset.y() ) / gridRes + 0.5 ); //NOLINT
double xSnapped = xRatio * gridRes + gridOffset.x();
double ySnapped = yRatio * gridRes + gridOffset.y() + yAtTopOfPage;
//convert snap tolerance from pixels to layout units
double alignThreshold = mTolerance / scaleFactor;
if ( fabs( xSnapped - point.x() ) > alignThreshold )
{
//snap distance is outside of tolerance
xSnapped = point.x();
}
else
{
snapped = true;
}
if ( fabs( ySnapped - point.y() ) > alignThreshold )
{
//snap distance is outside of tolerance
ySnapped = point.y();
}
else
{
snapped = true;
}
return QPointF( xSnapped, ySnapped );
}

View File

@ -21,6 +21,8 @@
#include "qgslayoutpoint.h"
#include <QPen>
class QgsLayout;
/**
* \ingroup core
* \class QgsLayoutSnapper
@ -41,7 +43,22 @@ class CORE_EXPORT QgsLayoutSnapper
GridCrosses //! Crosses
};
QgsLayoutSnapper();
/**
* Constructor for QgsLayoutSnapper, attached to the specified \a layout.
*/
QgsLayoutSnapper( QgsLayout *layout );
/**
* Sets the snap \a tolerance (in pixels) to use when snapping.
* \see snapTolerance()
*/
void setSnapTolerance( const int snapTolerance ) { mTolerance = snapTolerance; }
/**
* Returns the snap tolerance (in pixels) to use when snapping.
* \see setSnapTolerance()
*/
int snapTolerance() const { return mTolerance; }
/**
* Sets the page/snap grid \a resolution.
@ -99,13 +116,55 @@ class CORE_EXPORT QgsLayoutSnapper
*/
GridStyle gridStyle() const { return mGridStyle; }
/**
* Returns true if snapping to grid is enabled.
* \see setSnapToGrid()
*/
bool snapToGrid() const { return mSnapToGrid; }
/**
* Sets whether snapping to grid is \a enabled.
* \see snapToGrid()
*/
void setSnapToGrid( bool enabled ) { mSnapToGrid = enabled; }
/**
* Snaps a layout coordinate \a point. If \a point was snapped, \a snapped will be set to true.
*
* The \a scaleFactor argument should be set to the transformation from
* scalar transform from layout coordinates to pixels, i.e. the
* graphics view transform().m11() value.
*
* This method considers snapping to the grid, snap lines, etc.
*/
QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped SIP_OUT ) const;
/**
* Snaps a layout coordinate \a point to the grid. If \a point
* was snapped, \a snapped will be set to true.
*
* The \a scaleFactor argument should be set to the transformation from
* scalar transform from layout coordinates to pixels, i.e. the
* graphics view transform().m11() value.
*
* If snapToGrid() is disabled, this method will return the point
* unchanged.
*/
QPointF snapPointToGrid( QPointF point, double scaleFactor, bool &snapped SIP_OUT ) const;
private:
QgsLayout *mLayout = nullptr;
int mTolerance = 5;
QgsLayoutMeasurement mGridResolution;
QgsLayoutPoint mGridOffset;
QPen mGridPen;
GridStyle mGridStyle = GridLines;
bool mSnapToGrid = false;
};
#endif //QGSLAYOUTSNAPPER_H

View File

@ -19,8 +19,9 @@ from qgis.core import (QgsProject,
QgsLayoutSnapper,
QgsLayoutMeasurement,
QgsUnitTypes,
QgsLayoutPoint)
from qgis.PyQt.QtCore import QRectF
QgsLayoutPoint,
QgsLayoutItemPage)
from qgis.PyQt.QtCore import QRectF, QPointF
from qgis.PyQt.QtGui import (QTransform,
QPen,
QColor)
@ -33,7 +34,9 @@ start_app()
class TestQgsLayoutSnapper(unittest.TestCase):
def testGettersSetters(self):
s = QgsLayoutSnapper()
p = QgsProject()
l = QgsLayout(p)
s = QgsLayoutSnapper(l)
s.setGridResolution(QgsLayoutMeasurement(5, QgsUnitTypes.LayoutPoints))
self.assertEqual(s.gridResolution().length(), 5.0)
self.assertEqual(s.gridResolution().units(), QgsUnitTypes.LayoutPoints)
@ -49,6 +52,95 @@ class TestQgsLayoutSnapper(unittest.TestCase):
s.setGridStyle(QgsLayoutSnapper.GridDots)
self.assertEqual(s.gridStyle(), QgsLayoutSnapper.GridDots)
s.setSnapToGrid(False)
self.assertFalse(s.snapToGrid())
s.setSnapToGrid(True)
self.assertTrue(s.snapToGrid())
s.setSnapTolerance(15)
self.assertEqual(s.snapTolerance(), 15)
def testSnapPointToGrid(self):
p = QgsProject()
l = QgsLayout(p)
# need a page to snap to grid
page = QgsLayoutItemPage(l)
page.setPageSize('A4')
l.pageCollection().addPage(page)
s = QgsLayoutSnapper(l)
s.setGridResolution(QgsLayoutMeasurement(5, QgsUnitTypes.LayoutMillimeters))
s.setSnapToGrid(True)
s.setSnapTolerance(1)
point, snapped = s.snapPointToGrid(QPointF(1, 1), 1)
self.assertTrue(snapped)
self.assertEqual(point, QPointF(0, 0))
point, snapped = s.snapPointToGrid(QPointF(9, 1), 1)
self.assertTrue(snapped)
self.assertEqual(point, QPointF(10, 0))
point, snapped = s.snapPointToGrid(QPointF(1, 11), 1)
self.assertTrue(snapped)
self.assertEqual(point, QPointF(0, 10))
point, snapped = s.snapPointToGrid(QPointF(13, 11), 1)
self.assertTrue(snapped)
self.assertEqual(point, QPointF(13, 10))
point, snapped = s.snapPointToGrid(QPointF(11, 13), 1)
self.assertTrue(snapped)
self.assertEqual(point, QPointF(10, 13))
point, snapped = s.snapPointToGrid(QPointF(13, 23), 1)
self.assertFalse(snapped)
self.assertEqual(point, QPointF(13, 23))
# grid disabled
s.setSnapToGrid(False)
point, snapped = s.snapPointToGrid(QPointF(1, 1), 1)
self.assertFalse(snapped)
self.assertEqual(point, QPointF(1, 1))
s.setSnapToGrid(True)
# with different pixel scale
point, snapped = s.snapPointToGrid(QPointF(0.5, 0.5), 1)
self.assertTrue(snapped)
self.assertEqual(point, QPointF(0, 0))
point, snapped = s.snapPointToGrid(QPointF(0.5, 0.5), 3)
self.assertFalse(snapped)
self.assertEqual(point, QPointF(0.5, 0.5))
# with offset grid
s.setGridOffset(QgsLayoutPoint(2, 0))
point, snapped = s.snapPointToGrid(QPointF(13, 23), 1)
self.assertTrue(snapped)
self.assertEqual(point, QPointF(12, 23))
def testSnapPoint(self):
p = QgsProject()
l = QgsLayout(p)
page = QgsLayoutItemPage(l)
page.setPageSize('A4')
l.pageCollection().addPage(page)
s = QgsLayoutSnapper(l)
# first test snapping to grid
s.setGridResolution(QgsLayoutMeasurement(5, QgsUnitTypes.LayoutMillimeters))
s.setSnapToGrid(True)
s.setSnapTolerance(1)
point, snapped = s.snapPoint(QPointF(1, 1), 1)
self.assertTrue(snapped)
self.assertEqual(point, QPointF(0, 0))
s.setSnapToGrid(False)
point, snapped = s.snapPoint(QPointF(1, 1), 1)
self.assertFalse(snapped)
self.assertEqual(point, QPointF(1, 1))
if __name__ == '__main__':
unittest.main()