在不重叠的情况下围绕其他项目扩展图形项目移动

Extend graphics item movement around other items without overlap

我有一个带有可移动 QGraphicsEllipseitem 圆圈的图形场景。我试图通过让正在拖动的圆圈绕着它碰撞到的其他圆圈移动来防止圆圈重叠。到目前为止,它适用于 1 个碰撞项目来设置最小距离。

我正在尝试扩展 len(colliding)==1 的代码以适用于 2 个碰撞项目,因此我尝试将代码应用于每个碰撞项目。我先应用重叠较多的那个,然后再应用第二个。

当碰撞项目大小相同时,它部分起作用,因为它在没有重叠的情况下四处移动,但它周围有很多“故障”,所以我知道它并不完美。 但是当它们的尺寸不同时,它根本不起作用并且仍然重叠。 我不知道如何解决它。

class Circleitem(QGraphicsEllipseItem):

    def __init__(self, size, brush):
        super().__init__()
        radius = size / -2
        self.setRect(radius, radius, size, size)
        self.setBrush(brush)
        self.setFlag(self.ItemIsMovable)
        self.setFlag(self.ItemIsSelectable)

    def paint(self, painter, option, a):
        option.state = QStyle.State_None
        return super(Circleitem, self).paint(painter,option)

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)
        colliding = self.collidingItems()
        
        if len(colliding)==1:
            item = colliding[0]
            line = QLineF(item.pos(), self.pos() + QPoint(self.pos() == item.pos(), 0))
            min_distance = (self.rect().width() + item.rect().width()) / 2
            if line.length() < min_distance:
                line.setLength(min_distance)
                self.setPos(line.p2())

        elif len(colliding)==2:
            item0 = colliding[0]
            item1 = colliding[1]
            line0 = QLineF(item0.pos(), self.pos())
            line1 = QLineF(item1.pos(), self.pos())
            
            if line0.length() < line1.length():
                
                mindist = (self.rect().width() + item0.rect().width()) / 2
                if line0.length() < mindist:
                    line0.setLength(mindist)
                    self.setPos(line0.p2())

                second = item1
            else:
                mindist = (self.rect().width() + item1.rect().width()) / 2
            
                if line1.length() < mindist:
                    line1.setLength(mindist)
                    self.setPos(line1.p2())

                second = item0

            
            line = QLineF(second.pos(), self.pos())
            min_distance = (self.rect().width() + second.rect().width()) / 2
            if line.length() < min_distance:
                line.setLength(min_distance)
                self.setPos(line.p2())
            

    
class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.gscene = QGraphicsScene(0, 0, 1000, 1000)
        gview = QGraphicsView(self.gscene)
        self.setCentralWidget(gview)
        self.circle1 = Circleitem (123, brush=QColor(255,255,0))
        self.circle2 =Circleitem(80, brush=QColor(0,255,0))
        self.circle3 =Circleitem(80, brush=QColor(0,255,0))
        self.gscene.addItem(self.circle1)
        self.gscene.addItem(self.circle2)
        self.gscene.addItem(self.circle3)
        self.circle1.setPos(500, 500)
        self.circle2.setPos(300, 300)
        self.circle3.setPos(300, 400)
        self.show()



app = QApplication([])
win = MainWindow()
app.exec()

您可以通过获取移动圆与两个碰撞圆相切的点将其扩展到两个碰撞项目。

  • 假设你的圆 R 的半径为 r 并且与另外两个圆相撞。另外两个圆的圆心为 A、B,半径为 a、b(以黑色显示)。

  • 圆R和A之间的最小距离是它们的半径之和(r + a), 所以圆 R 与 A 相切,当它的圆心是沿给定圆心 A 和半径 r + a(以灰色显示)的圆上的任意点时。

  • 同理,R与B相切于以B为圆心,半径为r+b的圆上的任意一点。

  • 为了尊重两个距离,您正在寻找位于两个灰色圆圈上的点,即交点 I 和 J。

因此,如果圆R的圆心位于I点或J点,则圆R将同时与圆A和B相切。(选择离您当前位置较近的那个)。公式的解释在 两个圆的交点this page

class Circleitem(QGraphicsEllipseItem):

    def __init__(self, size, brush):
        super().__init__()
        radius = size / -2
        self.setRect(radius, radius, size, size)
        self.setBrush(brush)
        self.setFlag(self.ItemIsMovable)
        self.setFlag(self.ItemIsSelectable)
        self.last = Info()

    def paint(self, painter, option, a):
        option.state = QStyle.State_None
        return super(Circleitem, self).paint(painter,option)

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)
        if len(self.last.items) == 2 and self.last.intersects(self.pos()):
            return self.setPos(self.last.pos)
        
        colliding = self.collidingItems()
        if len(colliding) == 1:
            item = colliding[0]
            line = QLineF(item.pos(), self.pos() + QPoint(item.pos() == self.pos(), 0))
            min_distance = (self.rect().width() + item.rect().width()) / 2
            if line.length() < min_distance:
                line.setLength(min_distance)
                self.setPos(line.p2())
                
                colliding = self.collidingItems()
                if len(colliding) >= 2:
                    i, j = self.tangentPositions(*colliding[:2])
                    self.setPos(self.closest(self.pos(), i, j))
                                        
        elif len(colliding) >= 2:
            i, j = self.tangentPositions(*colliding[:2])
            self.setPos(self.closest(self.pos(), i, j))

        self.last.update(colliding[:2], self.pos())

    def closest(self, pos, i, j):
        return i if QLineF(pos, i).length() < QLineF(pos, j).length() else j

    def tangentPositions(self, A, B):
        r = self.rect().width() / 2
        rA = r + A.rect().width() / 2
        rB = r + B.rect().width() / 2
        A = A.pos(); B = B.pos()
        d = QLineF(A, B).length()
        
        cd = (rA ** 2 - rB ** 2 + d ** 2) / (2 * d)         # chord distance
        h = abs(rA ** 2 - cd ** 2) ** 0.5 *(-1*(cd>rA)|1)   # half chord length
        mid = A + cd * (B - A) / d                          # chord midpoint
        dx = h * (B - A).y() / d
        dy = h * (B - A).x() / d
        return mid + QPointF(dx, -dy), mid + QPointF(-dx, dy)


class Info:
    
    def __init__(self):
        self.update([], None)
        
    def update(self, items, pos):
        self.items = [x.pos() for x in items]
        self.pos = pos

    def intersects(self, point):
        return (QLineF(self.pos, point).intersect( # Qt 5.14+ use intersects()
            QLineF(*self.items), QPoint()) == QLineF.BoundedIntersection or
                QPolygonF([self.pos, *self.items]).containsPoint(point, 0))