Skip to content

Commit

Permalink
Add ability for QgsLayoutSnapper to snap to grid
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Aug 7, 2017
1 parent 361dd31 commit 5be237f
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 7 deletions.
58 changes: 57 additions & 1 deletion python/core/layout/qgslayoutsnapper.sip
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
************************************************************************/



class QgsLayoutSnapper
{
%Docstring
Expand All @@ -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
Expand Down Expand Up @@ -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

};

/************************************************************************
Expand Down
1 change: 1 addition & 0 deletions src/core/layout/qgslayout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand Down
70 changes: 68 additions & 2 deletions src/core/layout/qgslayoutsnapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
61 changes: 60 additions & 1 deletion src/core/layout/qgslayoutsnapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
#include "qgslayoutpoint.h"
#include <QPen>

class QgsLayout;

/**
* \ingroup core
* \class QgsLayoutSnapper
Expand All @@ -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.
Expand Down Expand Up @@ -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
98 changes: 95 additions & 3 deletions tests/src/python/test_qgslayoutsnapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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.