【问题标题】:Extend graphics item movement around other items without overlap围绕其他项目扩展图形项目移动而不重叠
【发布时间】:2021-11-19 02:27:10
【问题描述】:

我有一个带有可移动 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()

【问题讨论】:

  • 避免形状(尤其是多个/不同形状)的碰撞并非易事。您必须根据许多方面,包括项目的先前位置,自行找到适合您需求的算法。有很多关于这个问题的研究,并且没有简单/绝对的解决方案。考虑两个可能碰撞项目的矩形的基本示例,以及无法容纳其中空间的移动矩形:该移动的行为应该是什么?移动的项目应该在哪一点停止或“跳跃”?那么与移动物品发生碰撞呢?
  • 椭圆使这变得更加困难,因为您必须考虑基于三角函数的碰撞,这只是假设您正在处理“常规”圆并且它们的轴是正交的。旋转的椭圆呢?请参阅有关 Collision detection 的维基百科文章。
  • 感谢您的解释,我知道这是一个难题。我试图一步一步地只处理圆圈并处理 2 次碰撞

标签: python pyqt5 qgraphicsscene


【解决方案1】:

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

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

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

  • 同样,R 在圆心 B 和半径 r + b 的任意一点与 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))

【讨论】:

  • 问题依然存在:未知个可能发生碰撞的项目呢?
  • @musicamante 问题只是询问如何“扩展 len(colliding)==1 的代码以适用于 2 个碰撞项目”。
  • 您解决了我的问题,但我不希望圆圈跳到另一个交叉点。比如说,如果黄色圆圈在 2 个果岭的右侧,并且您将鼠标向左移动,它不应该跳到左侧,因为绿色的圆圈挡住了它。
  • @drivereye 查看修订版
  • 真的非常感谢。我也会尝试更多的圈子。它也可以与任意数量的碰撞项目一起使用吗?
猜你喜欢
  • 2021-12-28
  • 2017-06-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多