Skip to content
Permalink
Browse files

Add ability for QgsLayoutSnapper to snap to grid

  • Loading branch information
nyalldawson committed Jul 24, 2017
1 parent 361dd31 commit 5be237fdd926f7e21fea39d184a1ef5f89ed88b8
@@ -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

};

/************************************************************************
@@ -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...
@@ -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 );
}
@@ -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
@@ -19,8 +19,9 @@
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 @@
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 @@ def testGettersSetters(self):
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()

0 comments on commit 5be237f

Please sign in to comment.
You can’t perform that action at this time.