Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[needs-docs] Refine snapping logic for layouts
Previously grids would always take precedence when both a grid
and guide were within tolerance of a point.

Now, guides will always take precedence - since they have been
manually set by users we make the assumption that they have
been explicitly placed at highly desirable snapping locations,
and should be selected over the general grid.

Additionally, grid snapping was previously only done if BOTH
x and y could be snapped to the grid. We now snap to the nearest
grid line for x/y separately. This means if a point is close
to a vertical grid line but not a horizontal one it will still
snap to that nearby vertical grid line.
  • Loading branch information
nyalldawson committed Aug 7, 2017
1 parent 6d93411 commit e453116
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 44 deletions.
5 changes: 3 additions & 2 deletions python/core/layout/qgslayoutsnapper.sip
Expand Up @@ -77,10 +77,11 @@ class QgsLayoutSnapper
:rtype: QPointF
%End

QPointF snapPointToGrid( QPointF point, double scaleFactor, bool &snapped /Out/ ) const;
QPointF snapPointToGrid( QPointF point, double scaleFactor, bool &snappedX /Out/, bool &snappedY /Out/ ) const;
%Docstring
Snaps a layout coordinate ``point`` to the grid. If ``point``
was snapped, ``snapped`` will be set to true.
was snapped horizontally, ``snappedX`` will be set to true. If ``point``
was snapped vertically, ``snappedY`` 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
Expand Down
41 changes: 24 additions & 17 deletions src/core/layout/qgslayoutsnapper.cpp
Expand Up @@ -26,36 +26,43 @@ QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &sn
{
snapped = false;

// highest priority - grid
bool snappedToGrid = false;
QPointF res = snapPointToGrid( point, scaleFactor, snappedToGrid );
if ( snappedToGrid )
// highest priority - guides
bool snappedXToGuides = false;
double newX = snapPointToGuides( point.x(), QgsLayoutGuide::Vertical, scaleFactor, snappedXToGuides );
if ( snappedXToGuides )
{
snapped = true;
return res;
point.setX( newX );
}
bool snappedYToGuides = false;
double newY = snapPointToGuides( point.y(), QgsLayoutGuide::Horizontal, scaleFactor, snappedYToGuides );
if ( snappedYToGuides )
{
snapped = true;
point.setY( newY );
}

bool snappedToHozGuides = false;
double newX = snapPointToGuides( point.x(), QgsLayoutGuide::Vertical, scaleFactor, snappedToHozGuides );
if ( snappedToHozGuides )
bool snappedXToGrid = false;
bool snappedYToGrid = false;
QPointF res = snapPointToGrid( point, scaleFactor, snappedXToGrid, snappedYToGrid );
if ( snappedXToGrid && !snappedXToGuides )
{
snapped = true;
point.setX( newX );
point.setX( res.x() );
}
bool snappedToVertGuides = false;
double newY = snapPointToGuides( point.y(), QgsLayoutGuide::Horizontal, scaleFactor, snappedToVertGuides );
if ( snappedToVertGuides )
if ( snappedYToGrid && !snappedYToGuides )
{
snapped = true;
point.setY( newY );
point.setY( res.y() );
}

return point;
}

QPointF QgsLayoutSnapper::snapPointToGrid( QPointF point, double scaleFactor, bool &snapped ) const
QPointF QgsLayoutSnapper::snapPointToGrid( QPointF point, double scaleFactor, bool &snappedX, bool &snappedY ) const
{
snapped = false;
snappedX = false;
snappedY = false;
if ( !mLayout || !mSnapToGrid )
{
return point;
Expand Down Expand Up @@ -89,7 +96,7 @@ QPointF QgsLayoutSnapper::snapPointToGrid( QPointF point, double scaleFactor, bo
}
else
{
snapped = true;
snappedX = true;
}
if ( fabs( ySnapped - point.y() ) > alignThreshold )
{
Expand All @@ -98,7 +105,7 @@ QPointF QgsLayoutSnapper::snapPointToGrid( QPointF point, double scaleFactor, bo
}
else
{
snapped = true;
snappedY = true;
}

return QPointF( xSnapped, ySnapped );
Expand Down
5 changes: 3 additions & 2 deletions src/core/layout/qgslayoutsnapper.h
Expand Up @@ -90,7 +90,8 @@ class CORE_EXPORT QgsLayoutSnapper

/**
* Snaps a layout coordinate \a point to the grid. If \a point
* was snapped, \a snapped will be set to true.
* was snapped horizontally, \a snappedX will be set to true. If \a point
* was snapped vertically, \a snappedY 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
Expand All @@ -99,7 +100,7 @@ class CORE_EXPORT QgsLayoutSnapper
* If snapToGrid() is disabled, this method will return the point
* unchanged.
*/
QPointF snapPointToGrid( QPointF point, double scaleFactor, bool &snapped SIP_OUT ) const;
QPointF snapPointToGrid( QPointF point, double scaleFactor, bool &snappedX SIP_OUT, bool &snappedY SIP_OUT ) const;

/**
* Snaps a layout coordinate \a point to the grid. If \a point
Expand Down
107 changes: 84 additions & 23 deletions tests/src/python/test_qgslayoutsnapper.py
Expand Up @@ -21,10 +21,9 @@
QgsLayoutMeasurement,
QgsUnitTypes,
QgsLayoutPoint,
QgsLayoutItemPage)
QgsLayoutItemPage,
QgsLayoutGuide)
from qgis.PyQt.QtCore import QPointF
from qgis.PyQt.QtGui import (QPen,
QColor)

from qgis.testing import start_app, unittest

Expand Down Expand Up @@ -65,58 +64,112 @@ def testSnapPointToGrid(self):
s.setSnapToGrid(True)
s.setSnapTolerance(1)

point, snapped = s.snapPointToGrid(QPointF(1, 1), 1)
self.assertTrue(snapped)
point, snappedX, snappedY = s.snapPointToGrid(QPointF(1, 1), 1)
self.assertTrue(snappedX)
self.assertTrue(snappedY)
self.assertEqual(point, QPointF(0, 0))

point, snapped = s.snapPointToGrid(QPointF(9, 1), 1)
self.assertTrue(snapped)
point, snappedX, snappedY = s.snapPointToGrid(QPointF(9, 1), 1)
self.assertTrue(snappedX)
self.assertTrue(snappedY)
self.assertEqual(point, QPointF(10, 0))

point, snapped = s.snapPointToGrid(QPointF(1, 11), 1)
self.assertTrue(snapped)
point, snappedX, snappedY = s.snapPointToGrid(QPointF(1, 11), 1)
self.assertTrue(snappedX)
self.assertTrue(snappedY)
self.assertEqual(point, QPointF(0, 10))

point, snapped = s.snapPointToGrid(QPointF(13, 11), 1)
self.assertTrue(snapped)
point, snappedX, snappedY = s.snapPointToGrid(QPointF(13, 11), 1)
self.assertFalse(snappedX)
self.assertTrue(snappedY)
self.assertEqual(point, QPointF(13, 10))

point, snapped = s.snapPointToGrid(QPointF(11, 13), 1)
self.assertTrue(snapped)
point, snappedX, snappedY = s.snapPointToGrid(QPointF(11, 13), 1)
self.assertTrue(snappedX)
self.assertFalse(snappedY)
self.assertEqual(point, QPointF(10, 13))

point, snapped = s.snapPointToGrid(QPointF(13, 23), 1)
self.assertFalse(snapped)
point, snappedX, snappedY = s.snapPointToGrid(QPointF(13, 23), 1)
self.assertFalse(snappedX)
self.assertFalse(snappedY)
self.assertEqual(point, QPointF(13, 23))

# grid disabled
s.setSnapToGrid(False)
point, snapped = s.snapPointToGrid(QPointF(1, 1), 1)
self.assertFalse(snapped)
point, nappedX, snappedY = s.snapPointToGrid(QPointF(1, 1), 1)
self.assertFalse(nappedX)
self.assertFalse(snappedY)
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)
point, snappedX, snappedY = s.snapPointToGrid(QPointF(0.5, 0.5), 1)
self.assertTrue(snappedX)
self.assertTrue(snappedY)
self.assertEqual(point, QPointF(0, 0))
point, snapped = s.snapPointToGrid(QPointF(0.5, 0.5), 3)
self.assertFalse(snapped)
point, snappedX, snappedY = s.snapPointToGrid(QPointF(0.5, 0.5), 3)
self.assertFalse(snappedX)
self.assertFalse(snappedY)
self.assertEqual(point, QPointF(0.5, 0.5))

# with offset grid
l.gridSettings().setOffset(QgsLayoutPoint(2, 0))
point, snapped = s.snapPointToGrid(QPointF(13, 23), 1)
self.assertTrue(snapped)
point, snappedX, snappedY = s.snapPointToGrid(QPointF(13, 23), 1)
self.assertTrue(snappedX)
self.assertFalse(snappedY)
self.assertEqual(point, QPointF(12, 23))

def testSnapPointToGuides(self):
p = QgsProject()
l = QgsLayout(p)
page = QgsLayoutItemPage(l)
page.setPageSize('A4')
l.pageCollection().addPage(page)
s = QgsLayoutSnapper(l)
guides = l.guides()

s.setSnapToGuides(True)
s.setSnapTolerance(1)

# no guides
point, snapped = s.snapPointToGuides(0.5, QgsLayoutGuide.Vertical, 1)
self.assertFalse(snapped)

guides.addGuide(QgsLayoutGuide(QgsLayoutGuide.Vertical, QgsLayoutMeasurement(1)))
point, snapped = s.snapPointToGuides(0.5, QgsLayoutGuide.Vertical, 1)
self.assertTrue(snapped)
self.assertEqual(point, 1)

# outside tolerance
point, snapped = s.snapPointToGuides(5.5, QgsLayoutGuide.Vertical, 1)
self.assertFalse(snapped)

# snapping off
s.setSnapToGuides(False)
point, snapped = s.snapPointToGuides(0.5, QgsLayoutGuide.Vertical, 1)
self.assertFalse(snapped)

s.setSnapToGuides(True)
# snap to hoz
point, snapped = s.snapPointToGuides(0.5, QgsLayoutGuide.Horizontal, 1)
self.assertFalse(snapped)
guides.addGuide(QgsLayoutGuide(QgsLayoutGuide.Horizontal, QgsLayoutMeasurement(1)))
point, snapped = s.snapPointToGuides(0.5, QgsLayoutGuide.Horizontal, 1)
self.assertTrue(snapped)
self.assertEqual(point, 1)

# with different pixel scale
point, snapped = s.snapPointToGuides(0.5, QgsLayoutGuide.Horizontal, 3)
self.assertFalse(snapped)

def testSnapPoint(self):
p = QgsProject()
l = QgsLayout(p)
page = QgsLayoutItemPage(l)
page.setPageSize('A4')
l.pageCollection().addPage(page)
s = QgsLayoutSnapper(l)
guides = l.guides()

# first test snapping to grid
l.gridSettings().setResolution(QgsLayoutMeasurement(5, QgsUnitTypes.LayoutMillimeters))
Expand All @@ -132,6 +185,14 @@ def testSnapPoint(self):
self.assertFalse(snapped)
self.assertEqual(point, QPointF(1, 1))

# test that guide takes precedence
s.setSnapToGrid(True)
s.setSnapToGuides(True)
guides.addGuide(QgsLayoutGuide(QgsLayoutGuide.Horizontal, QgsLayoutMeasurement(0.5)))
point, snapped = s.snapPoint(QPointF(1, 1), 1)
self.assertTrue(snapped)
self.assertEqual(point, QPointF(0, 0.5))


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

0 comments on commit e453116

Please sign in to comment.