1

I want to allow a QGraphicsItem to be dragged only in certain directions, such as +/-45 degrees, horizontally or vertically, and to be able to "jump" to a new direction once the cursor is dragged far enough away from the current closest direction. This would replicate behaviour in e.g. Inkscape when drawing a straight line and holding Ctrl (see e.g. this video), but I am unsure how to implement it.

I've implemented a drag handler that grabs the new position of the item as it is moved:

class Circle(QGraphicsEllipseItem):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Flags to allow dragging and tracking of dragging.
        self.setFlag(self.ItemSendsGeometryChanges)
        self.setFlag(self.ItemIsMovable)
        self.setFlag(self.ItemIsSelectable)

    def itemChange(self, change, value):
        if change == self.ItemPositionChange and self.isSelected():
            # do something...

        # Return the new position to parent to have this item move there.
        return super().itemChange(change, value)

Since the position returned to the parent by this method is used to update the position of the item in the scene, I expect that I can modify this QPointF to limit it to one axis, but I am unsure how to do so in a way that lets the line "jump" to another direction once the cursor is dragged far enough. Are there any "standard algorithms" for this sort of behaviour? Or perhaps some built-in Qt code that can do this for me?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Sean
  • 1,346
  • 13
  • 24

1 Answers1

2

The problem is reduced to calculate the projection of the point (position of the item) on the line. Doing a little math as explained in this post.

Let p1 and p2 be two different points on the line and p the point then the algorithm is:

e1 = p2 - p1
e2 = p - p1
dp = e1 • e2 # dot product
l  = e1 • e1 # dot product
pp = p1 + dp * e1 / l

Implementing the above the solution is:

import math
import random
from PyQt5 import QtCore, QtGui, QtWidgets


class Circle(QtWidgets.QGraphicsEllipseItem):
    def __init__(self, *args, **kwargs):
        self._line = QtCore.QLineF()
        super().__init__(*args, **kwargs)
        # Flags to allow dragging and tracking of dragging.
        self.setFlags(
            self.flags()
            | QtWidgets.QGraphicsItem.ItemSendsGeometryChanges
            | QtWidgets.QGraphicsItem.ItemIsMovable
            | QtWidgets.QGraphicsItem.ItemIsSelectable
        )

    @property
    def line(self):
        return self._line

    @line.setter
    def line(self, line):
        self._line = line

    def itemChange(self, change, value):
        if (
            change == QtWidgets.QGraphicsItem.ItemPositionChange
            and self.isSelected()
            and not self.line.isNull()
        ):
            # http://www.sunshine2k.de/coding/java/PointOnLine/PointOnLine.html
            p1 = self.line.p1()
            p2 = self.line.p2()
            e1 = p2 - p1
            e2 = value - p1
            dp = QtCore.QPointF.dotProduct(e1, e2)
            l = QtCore.QPointF.dotProduct(e1, e1)
            p = p1 + dp * e1 / l
            return p
        return super().itemChange(change, value)


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)

    scene = QtWidgets.QGraphicsScene(QtCore.QRectF(-200, -200, 400, 400))
    view = QtWidgets.QGraphicsView(scene)

    points = (
        QtCore.QPointF(*random.sample(range(-150, 150), 2)) for _ in range(4)
    )
    angles = (math.pi / 4, math.pi / 3, math.pi / 5, math.pi / 2)

    for point, angle in zip(points, angles):
        item = Circle(QtCore.QRectF(-10, -10, 20, 20))
        item.setBrush(QtGui.QColor("salmon"))
        scene.addItem(item)
        item.setPos(point)
        end = 100 * QtCore.QPointF(math.cos(angle), math.sin(angle))
        line = QtCore.QLineF(QtCore.QPointF(), end)
        item.line = line.translated(item.pos())
        line_item = scene.addLine(item.line)
        line_item.setPen(QtGui.QPen(QtGui.QColor("green"), 4))

    view.resize(640, 480)
    view.show()

    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Wow, that's great, thanks! In your example, is the constrained drag direction set by the initial line direction? – Sean Jun 11 '19 at 20:45
  • 1
    @Sean I have improved my example, now you just have to set the angle in radians. In my example the item has a random position so the line will adjust to the initial point – eyllanesc Jun 11 '19 at 20:58