Skip to content

Commit 5be237f

Browse files
committedAug 7, 2017
Add ability for QgsLayoutSnapper to snap to grid
1 parent 361dd31 commit 5be237f

File tree

5 files changed

+281
-7
lines changed

5 files changed

+281
-7
lines changed
 

‎python/core/layout/qgslayoutsnapper.sip

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
************************************************************************/
88

99

10+
1011
class QgsLayoutSnapper
1112
{
1213
%Docstring
@@ -27,7 +28,23 @@ class QgsLayoutSnapper
2728
GridCrosses
2829
};
2930

30-
QgsLayoutSnapper();
31+
QgsLayoutSnapper( QgsLayout *layout );
32+
%Docstring
33+
Constructor for QgsLayoutSnapper, attached to the specified ``layout``.
34+
%End
35+
36+
void setSnapTolerance( const int snapTolerance );
37+
%Docstring
38+
Sets the snap ``tolerance`` (in pixels) to use when snapping.
39+
.. seealso:: snapTolerance()
40+
%End
41+
42+
int snapTolerance() const;
43+
%Docstring
44+
Returns the snap tolerance (in pixels) to use when snapping.
45+
.. seealso:: setSnapTolerance()
46+
:rtype: int
47+
%End
3148

3249
void setGridResolution( const QgsLayoutMeasurement &resolution );
3350
%Docstring
@@ -89,6 +106,45 @@ class QgsLayoutSnapper
89106
:rtype: GridStyle
90107
%End
91108

109+
bool snapToGrid() const;
110+
%Docstring
111+
Returns true if snapping to grid is enabled.
112+
.. seealso:: setSnapToGrid()
113+
:rtype: bool
114+
%End
115+
116+
void setSnapToGrid( bool enabled );
117+
%Docstring
118+
Sets whether snapping to grid is ``enabled``.
119+
.. seealso:: snapToGrid()
120+
%End
121+
122+
QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped /Out/ ) const;
123+
%Docstring
124+
Snaps a layout coordinate ``point``. If ``point`` was snapped, ``snapped`` will be set to true.
125+
126+
The ``scaleFactor`` argument should be set to the transformation from
127+
scalar transform from layout coordinates to pixels, i.e. the
128+
graphics view transform().m11() value.
129+
130+
This method considers snapping to the grid, snap lines, etc.
131+
:rtype: QPointF
132+
%End
133+
134+
QPointF snapPointToGrid( QPointF point, double scaleFactor, bool &snapped /Out/ ) const;
135+
%Docstring
136+
Snaps a layout coordinate ``point`` to the grid. If ``point``
137+
was snapped, ``snapped`` will be set to true.
138+
139+
The ``scaleFactor`` argument should be set to the transformation from
140+
scalar transform from layout coordinates to pixels, i.e. the
141+
graphics view transform().m11() value.
142+
143+
If snapToGrid() is disabled, this method will return the point
144+
unchanged.
145+
:rtype: QPointF
146+
%End
147+
92148
};
93149

94150
/************************************************************************

‎src/core/layout/qgslayout.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
QgsLayout::QgsLayout( QgsProject *project )
2121
: QGraphicsScene()
2222
, mProject( project )
23+
, mSnapper( QgsLayoutSnapper( this ) )
2324
, mPageCollection( new QgsLayoutPageCollection( this ) )
2425
{
2526
// just to make sure - this should be the default, but maybe it'll change in some future Qt version...

‎src/core/layout/qgslayoutsnapper.cpp

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,76 @@
1515
***************************************************************************/
1616

1717
#include "qgslayoutsnapper.h"
18+
#include "qgslayout.h"
1819

19-
QgsLayoutSnapper::QgsLayoutSnapper()
20-
: mGridResolution( QgsLayoutMeasurement( 10 ) )
20+
QgsLayoutSnapper::QgsLayoutSnapper( QgsLayout *layout )
21+
: mLayout( layout )
22+
, mGridResolution( QgsLayoutMeasurement( 10 ) )
2123
{
2224
mGridPen = QPen( QColor( 190, 190, 190, 100 ), 0 );
2325
mGridPen.setCosmetic( true );
2426
}
27+
28+
QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &snapped ) const
29+
{
30+
snapped = false;
31+
32+
// highest priority - grid
33+
bool snappedToGrid = false;
34+
QPointF res = snapPointToGrid( point, scaleFactor, snappedToGrid );
35+
if ( snappedToGrid )
36+
{
37+
snapped = true;
38+
return res;
39+
}
40+
41+
return point;
42+
}
43+
44+
QPointF QgsLayoutSnapper::snapPointToGrid( QPointF point, double scaleFactor, bool &snapped ) const
45+
{
46+
snapped = false;
47+
if ( !mLayout || !mSnapToGrid || mGridResolution.length() <= 0 )
48+
{
49+
return point;
50+
}
51+
52+
//calculate y offset to current page
53+
QPointF pagePoint = mLayout->pageCollection()->positionOnPage( point );
54+
55+
double yPage = pagePoint.y(); //y-coordinate relative to current page
56+
double yAtTopOfPage = mLayout->pageCollection()->page( mLayout->pageCollection()->pageNumberForPoint( point ) )->pos().y();
57+
58+
//snap x coordinate
59+
double gridRes = mLayout->convertToLayoutUnits( mGridResolution );
60+
QPointF gridOffset = mLayout->convertToLayoutUnits( mGridOffset );
61+
int xRatio = static_cast< int >( ( point.x() - gridOffset.x() ) / gridRes + 0.5 ); //NOLINT
62+
int yRatio = static_cast< int >( ( yPage - gridOffset.y() ) / gridRes + 0.5 ); //NOLINT
63+
64+
double xSnapped = xRatio * gridRes + gridOffset.x();
65+
double ySnapped = yRatio * gridRes + gridOffset.y() + yAtTopOfPage;
66+
67+
//convert snap tolerance from pixels to layout units
68+
double alignThreshold = mTolerance / scaleFactor;
69+
70+
if ( fabs( xSnapped - point.x() ) > alignThreshold )
71+
{
72+
//snap distance is outside of tolerance
73+
xSnapped = point.x();
74+
}
75+
else
76+
{
77+
snapped = true;
78+
}
79+
if ( fabs( ySnapped - point.y() ) > alignThreshold )
80+
{
81+
//snap distance is outside of tolerance
82+
ySnapped = point.y();
83+
}
84+
else
85+
{
86+
snapped = true;
87+
}
88+
89+
return QPointF( xSnapped, ySnapped );
90+
}

‎src/core/layout/qgslayoutsnapper.h

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
#include "qgslayoutpoint.h"
2222
#include <QPen>
2323

24+
class QgsLayout;
25+
2426
/**
2527
* \ingroup core
2628
* \class QgsLayoutSnapper
@@ -41,7 +43,22 @@ class CORE_EXPORT QgsLayoutSnapper
4143
GridCrosses //! Crosses
4244
};
4345

44-
QgsLayoutSnapper();
46+
/**
47+
* Constructor for QgsLayoutSnapper, attached to the specified \a layout.
48+
*/
49+
QgsLayoutSnapper( QgsLayout *layout );
50+
51+
/**
52+
* Sets the snap \a tolerance (in pixels) to use when snapping.
53+
* \see snapTolerance()
54+
*/
55+
void setSnapTolerance( const int snapTolerance ) { mTolerance = snapTolerance; }
56+
57+
/**
58+
* Returns the snap tolerance (in pixels) to use when snapping.
59+
* \see setSnapTolerance()
60+
*/
61+
int snapTolerance() const { return mTolerance; }
4562

4663
/**
4764
* Sets the page/snap grid \a resolution.
@@ -99,13 +116,55 @@ class CORE_EXPORT QgsLayoutSnapper
99116
*/
100117
GridStyle gridStyle() const { return mGridStyle; }
101118

119+
/**
120+
* Returns true if snapping to grid is enabled.
121+
* \see setSnapToGrid()
122+
*/
123+
bool snapToGrid() const { return mSnapToGrid; }
124+
125+
/**
126+
* Sets whether snapping to grid is \a enabled.
127+
* \see snapToGrid()
128+
*/
129+
void setSnapToGrid( bool enabled ) { mSnapToGrid = enabled; }
130+
131+
/**
132+
* Snaps a layout coordinate \a point. If \a point was snapped, \a snapped will be set to true.
133+
*
134+
* The \a scaleFactor argument should be set to the transformation from
135+
* scalar transform from layout coordinates to pixels, i.e. the
136+
* graphics view transform().m11() value.
137+
*
138+
* This method considers snapping to the grid, snap lines, etc.
139+
*/
140+
QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped SIP_OUT ) const;
141+
142+
/**
143+
* Snaps a layout coordinate \a point to the grid. If \a point
144+
* was snapped, \a snapped will be set to true.
145+
*
146+
* The \a scaleFactor argument should be set to the transformation from
147+
* scalar transform from layout coordinates to pixels, i.e. the
148+
* graphics view transform().m11() value.
149+
*
150+
* If snapToGrid() is disabled, this method will return the point
151+
* unchanged.
152+
*/
153+
QPointF snapPointToGrid( QPointF point, double scaleFactor, bool &snapped SIP_OUT ) const;
154+
102155
private:
103156

157+
QgsLayout *mLayout = nullptr;
158+
159+
int mTolerance = 5;
160+
104161
QgsLayoutMeasurement mGridResolution;
105162
QgsLayoutPoint mGridOffset;
106163
QPen mGridPen;
107164
GridStyle mGridStyle = GridLines;
108165

166+
bool mSnapToGrid = false;
167+
109168
};
110169

111170
#endif //QGSLAYOUTSNAPPER_H

‎tests/src/python/test_qgslayoutsnapper.py

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
QgsLayoutSnapper,
2020
QgsLayoutMeasurement,
2121
QgsUnitTypes,
22-
QgsLayoutPoint)
23-
from qgis.PyQt.QtCore import QRectF
22+
QgsLayoutPoint,
23+
QgsLayoutItemPage)
24+
from qgis.PyQt.QtCore import QRectF, QPointF
2425
from qgis.PyQt.QtGui import (QTransform,
2526
QPen,
2627
QColor)
@@ -33,7 +34,9 @@
3334
class TestQgsLayoutSnapper(unittest.TestCase):
3435

3536
def testGettersSetters(self):
36-
s = QgsLayoutSnapper()
37+
p = QgsProject()
38+
l = QgsLayout(p)
39+
s = QgsLayoutSnapper(l)
3740
s.setGridResolution(QgsLayoutMeasurement(5, QgsUnitTypes.LayoutPoints))
3841
self.assertEqual(s.gridResolution().length(), 5.0)
3942
self.assertEqual(s.gridResolution().units(), QgsUnitTypes.LayoutPoints)
@@ -49,6 +52,95 @@ def testGettersSetters(self):
4952
s.setGridStyle(QgsLayoutSnapper.GridDots)
5053
self.assertEqual(s.gridStyle(), QgsLayoutSnapper.GridDots)
5154

55+
s.setSnapToGrid(False)
56+
self.assertFalse(s.snapToGrid())
57+
s.setSnapToGrid(True)
58+
self.assertTrue(s.snapToGrid())
59+
60+
s.setSnapTolerance(15)
61+
self.assertEqual(s.snapTolerance(), 15)
62+
63+
def testSnapPointToGrid(self):
64+
p = QgsProject()
65+
l = QgsLayout(p)
66+
# need a page to snap to grid
67+
page = QgsLayoutItemPage(l)
68+
page.setPageSize('A4')
69+
l.pageCollection().addPage(page)
70+
s = QgsLayoutSnapper(l)
71+
72+
s.setGridResolution(QgsLayoutMeasurement(5, QgsUnitTypes.LayoutMillimeters))
73+
s.setSnapToGrid(True)
74+
s.setSnapTolerance(1)
75+
76+
point, snapped = s.snapPointToGrid(QPointF(1, 1), 1)
77+
self.assertTrue(snapped)
78+
self.assertEqual(point, QPointF(0, 0))
79+
80+
point, snapped = s.snapPointToGrid(QPointF(9, 1), 1)
81+
self.assertTrue(snapped)
82+
self.assertEqual(point, QPointF(10, 0))
83+
84+
point, snapped = s.snapPointToGrid(QPointF(1, 11), 1)
85+
self.assertTrue(snapped)
86+
self.assertEqual(point, QPointF(0, 10))
87+
88+
point, snapped = s.snapPointToGrid(QPointF(13, 11), 1)
89+
self.assertTrue(snapped)
90+
self.assertEqual(point, QPointF(13, 10))
91+
92+
point, snapped = s.snapPointToGrid(QPointF(11, 13), 1)
93+
self.assertTrue(snapped)
94+
self.assertEqual(point, QPointF(10, 13))
95+
96+
point, snapped = s.snapPointToGrid(QPointF(13, 23), 1)
97+
self.assertFalse(snapped)
98+
self.assertEqual(point, QPointF(13, 23))
99+
100+
# grid disabled
101+
s.setSnapToGrid(False)
102+
point, snapped = s.snapPointToGrid(QPointF(1, 1), 1)
103+
self.assertFalse(snapped)
104+
self.assertEqual(point, QPointF(1, 1))
105+
s.setSnapToGrid(True)
106+
107+
# with different pixel scale
108+
point, snapped = s.snapPointToGrid(QPointF(0.5, 0.5), 1)
109+
self.assertTrue(snapped)
110+
self.assertEqual(point, QPointF(0, 0))
111+
point, snapped = s.snapPointToGrid(QPointF(0.5, 0.5), 3)
112+
self.assertFalse(snapped)
113+
self.assertEqual(point, QPointF(0.5, 0.5))
114+
115+
# with offset grid
116+
s.setGridOffset(QgsLayoutPoint(2, 0))
117+
point, snapped = s.snapPointToGrid(QPointF(13, 23), 1)
118+
self.assertTrue(snapped)
119+
self.assertEqual(point, QPointF(12, 23))
120+
121+
122+
def testSnapPoint(self):
123+
p = QgsProject()
124+
l = QgsLayout(p)
125+
page = QgsLayoutItemPage(l)
126+
page.setPageSize('A4')
127+
l.pageCollection().addPage(page)
128+
s = QgsLayoutSnapper(l)
129+
130+
# first test snapping to grid
131+
s.setGridResolution(QgsLayoutMeasurement(5, QgsUnitTypes.LayoutMillimeters))
132+
s.setSnapToGrid(True)
133+
s.setSnapTolerance(1)
134+
135+
point, snapped = s.snapPoint(QPointF(1, 1), 1)
136+
self.assertTrue(snapped)
137+
self.assertEqual(point, QPointF(0, 0))
138+
139+
s.setSnapToGrid(False)
140+
point, snapped = s.snapPoint(QPointF(1, 1), 1)
141+
self.assertFalse(snapped)
142+
self.assertEqual(point, QPointF(1, 1))
143+
52144

53145
if __name__ == '__main__':
54146
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.