Avoiding collisions of QGraphicsItem shapes moved by the mouse

An interesting discussion was raised here about preventing collisions of circles, made of QGraphicsEllipseItems, in a QGraphicsScene. The question narrowed the scope to 2 colliding items but the larger goal still remained, what about for any number of collisions?

This is the desired behavior:

  • When one item is dragged over other items they should not overlap, instead it should move around those items as close as possible to the mouse.
  • It should not “teleport” if it gets blocked in by other items.
  • It should be a smooth and predictable movement.

As this becomes increasingly complex to find the best “safe” position for the circle while it’s moving I wanted to present another way to implement this using a physics simulator.

1 answer

  • answered 2021-11-23 02:01 alec

    Given the behavior described above it’s a good candidate for 2D rigid body physics, maybe it can be done without but it would be difficult to get it perfect. I am using pymunk in this example because I’m familiar with it but the same concepts will work with other libraries.

    The scene has a kinematic body to represent the mouse and the circles are represented by a static bodies initially. While a circle is selected it switches to a dynamic body and is constrained to the mouse by a damped spring. Its position is updated as the space is updated by a given time step on each timeout interval.

    The item is not actually moved in the same way as the ItemIsMovable flag is not enabled, which means it no longer moves instantly with the mouse. It’s very close but there’s a small delay, although you may prefer this to better see how it reacts to collisions. (Even so, you can fine-tune the parameters to have it move faster/closer to the mouse than I did**).

    On the other hand, the collisions are handled perfectly and will already support other kinds of shapes.

    import sys
    from PyQt5.QtWidgets import *
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    import pymunk
    
    class Circle(QGraphicsEllipseItem):
    
        def __init__(self, r, **kwargs):
            super().__init__(-r, -r, r * 2, r * 2, **kwargs)
            self.setFlags(QGraphicsItem.ItemIsSelectable)
            self.static = pymunk.Body(body_type=pymunk.Body.STATIC)
            self.circle = pymunk.Circle(self.static, r)
            self.circle.friction = 0
            mass = 10
            self.dynamic = pymunk.Body(mass, pymunk.moment_for_circle(mass, 0, r))
            self.updatePos = lambda: self.setPos(*self.dynamic.position, dset=False)
    
        def setPos(self, *pos, dset=True):
            super().setPos(*pos)
            if len(pos) == 1:
                pos = pos[0].x(), pos[0].y()
            self.static.position = pos
            if dset:
                self.dynamic.position = pos
    
        def itemChange(self, change, value):
            if change == QGraphicsItem.ItemSelectedChange:
                space = self.circle.space
                space.remove(self.circle.body, self.circle)
                self.circle.body = self.dynamic if value else self.static
                space.add(self.circle.body, self.circle)
            return super().itemChange(change, value)
    
        def paint(self, painter, option, widget):
            option.state &= ~QStyle.State_Selected
            super().paint(painter, option, widget)
    
    
    class Scene(QGraphicsScene):
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.space = pymunk.Space()
            self.space.damping = 0.02
            self.body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
            self.space.add(self.body)
            self.timer = QTimer(self, timerType=Qt.PreciseTimer, timeout=self.step)
            self.selectionChanged.connect(self.setConstraint)
    
        def setConstraint(self):
            selected = self.selectedItems()
            if selected:
                shape = selected[0].circle
                if not shape.body.constraints:
                    self.space.remove(*self.space.constraints)
                    spring = pymunk.DampedSpring(
                        self.body, shape.body, (0, 0), (0, 0),
                        rest_length=0, stiffness=100, damping=10)
                    spring.collide_bodies = False
                    self.space.add(spring)
    
        def step(self):
            for i in range(10):
                self.space.step(1 / 30)
            self.selectedItems()[0].updatePos()
    
        def mousePressEvent(self, event):
            super().mousePressEvent(event)
            if self.selectedItems():
                self.body.position = event.scenePos().x(), event.scenePos().y()
                self.timer.start(1000 / 30)
                
        def mouseMoveEvent(self, event):            
            super().mouseMoveEvent(event)
            if self.selectedItems():
                self.body.position = event.scenePos().x(), event.scenePos().y()
            
        def mouseReleaseEvent(self, event):
            super().mouseReleaseEvent(event)
            self.timer.stop()
    
        def addCircle(self, x, y, radius):
            item = Circle(radius)
            item.setPos(x, y)
            self.addItem(item)
            self.space.add(item.circle.body, item.circle)
            return item
    
    
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        scene = Scene(0, 0, 1000, 800)
        for i in range(7, 13):
            item = scene.addCircle(150 * (i - 6), 400, i * 5)
            item.setBrush(Qt.GlobalColor(i))    
        view = QGraphicsView(scene, renderHints=QPainter.Antialiasing)
        view.show()
        sys.exit(app.exec_())
    

    **Can adjust the following:

    • Spring stiffness and damping
    • Body mass and moment of inertia
    • Space damping
    • Space.step time step / how many calls per QTimer timeout
    • QTimer interval

How many English words
do you know?
Test your English vocabulary size, and measure
how many words do you know
Online Test
Powered by Examplum