Search code examples
pythonpyqtgeometrycollision-detection

collision detect between a rectangle and a slice of a circle


I made an object with three points of view. one for forward, one for left, and one for right.

enter image description here

these POVs are some path like a slice of a circle

enter image description here

I want to detect intersections or collisions between these POVs with rectangles to set the color of each POV

POVs rotate with the object in any direction but rectangles are always oriented

enter image description here enter image description here

here is my code

from random import randint
from sys import argv
from PyQt6.QtCore import QRectF, Qt, QTimer, QPoint
from PyQt6.QtGui import QColor, QKeyEvent, QMouseEvent, QPainter, QPen, QPaintEvent, QPainterPath, QBrush
from PyQt6.QtWidgets import QApplication, QVBoxLayout, QWidget


class Window(QWidget):
    def __init__(self, parent=None) -> None:
        super().__init__(parent)
        screenWidth = 1920
        screenHeight = 1080
        self.gX = []
        self.gY = []
        self.framesShowPerSecond = 30
        self.age = 0
        self.maxAge = 500
        self.windowWidth = 1920
        self.windowHeight = 1080
        self.isRunning = True
        self.angle = -90
        self.clockCounterVariable = 0
        self.milliseconds = 0
        self.seconds = 0
        self.minutes = 0
        self.hours = 0
        self.setWindowTitle("test")
        self.setGeometry((screenWidth - self.windowWidth) // 2, (screenHeight - self.windowHeight) // 2, self.windowWidth, self.windowHeight)
        self.setLayout(QVBoxLayout())
        self.showFullScreen()
        self.setStyleSheet("background-color:rgb(20, 20, 20);font-size:20px;")
        self.xCounter = 0
        self.clock = QTimer(self)
        self.graphicTimer = QTimer(self)
        self.clock.timeout.connect(self.clockCounter)
        self.graphicTimer.timeout.connect(self.update)
        self.graphicTimer.start(round((1/self.framesShowPerSecond)*1000))
        self.clock.start(10)
        self.show()

    def clockCounter(self) -> None:
        if self.clockCounterVariable % 10 == 0:
            x = self.xCounter
            y = randint(0, 100)
            self.xCounter += 1
            self.gX.append(x - 0.5)
            self.gX.append(x + 0.5)
            self.gY.append(y)
            self.gY.append(y)
        self.clockCounterVariable += 1

    def keyPressEvent(self, event: QKeyEvent) -> super:
        key = QKeyEvent.key(event)
        if key == 112 or key == 80: # P/p
            if self.isRunning:
                print("pause process")
                self.isRunning = False
                self.clock.stop()
                self.graphicTimer.stop()
            else:
                print("continue process")
                self.isRunning = True
                self.clock.start(1000)
                self.graphicTimer.start(round((1/self.framesShowPerSecond)*1000))
        elif (key == 115) or (key == 83): # S/s
            self.closeWindow()
        self.update()
        return super().keyPressEvent(event)

    def mousePressEvent(self, event: QMouseEvent) -> super:
        if event.buttons() == Qt.MouseButton.LeftButton:
            if self.isRunning:
                print("pause process")
                self.isRunning = False
                self.clock.stop()
                self.graphicTimer.stop()
            else:
                print("continue process")
                self.isRunning = True
                self.clock.start(1000)
                self.graphicTimer.start(round((1/self.framesShowPerSecond)*1000))
        return super().mousePressEvent(event)

    def paintEvent(self, event: QPaintEvent) -> super:
        self.milliseconds = self.clockCounterVariable
        self.seconds, self.milliseconds = divmod(self.milliseconds, 100)
        self.minutes, self.seconds = divmod(self.seconds, 60)
        self.hours, self.minutes = divmod(self.minutes, 60)
        painter = QPainter()
        painter.begin(self)
        painter.setPen(QPen(QColor(255, 128, 20),  1, Qt.PenStyle.SolidLine))
        painter.drawText(QRectF(35, 30, 400, 30), Qt.AlignmentFlag.AlignLeft, "{:02d} : {:02d} : {:02d} : {:02d}".format(self.hours, self.minutes, self.seconds, self.milliseconds))
        painter.setPen(QPen(QColor(20, 20, 20),  -1, Qt.PenStyle.SolidLine))
        painter.setBrush(QBrush(QColor(20, 20, 160), Qt.BrushStyle.SolidPattern))
        barrier = QRectF(1920//2-25, 1080//2-25-40, 50, 20)
        painter.drawRect(barrier)
        painter.translate(QPoint(1920//2, 1080//2))
        painter.rotate(self.angle)
        painter.setBrush(QBrush(QColor(200, 200, 200, 50), Qt.BrushStyle.SolidPattern))
        r = 200
        a = 40
        b = a * 2
        rect = QRectF(-r/2, -r/2, r, r)
        path = QPainterPath()
        path.arcTo(rect, -a, b)
        path.closeSubpath()
        if path.contains(barrier):
            painter.setBrush(QBrush(QColor(200, 20, 20, 50), Qt.BrushStyle.SolidPattern))
        else:
            painter.setBrush(QBrush(QColor(20, 200, 20, 50), Qt.BrushStyle.SolidPattern))
        painter.drawPath(path)
        path = QPainterPath()
        path.arcTo(rect, -a+90, b)
        path.closeSubpath()
        if path.contains(barrier):
            painter.setBrush(QBrush(QColor(200, 20, 20, 50), Qt.BrushStyle.SolidPattern))
        else:
            painter.setBrush(QBrush(QColor(20, 200, 20, 50), Qt.BrushStyle.SolidPattern))
        painter.drawPath(path)
        path = QPainterPath()
        path.arcTo(rect, -a-90, b)
        path.closeSubpath()
        if path.contains(barrier):
            painter.setBrush(QBrush(QColor(200, 20, 20, 50), Qt.BrushStyle.SolidPattern))
        else:
            painter.setBrush(QBrush(QColor(20, 200, 20, 50), Qt.BrushStyle.SolidPattern))
        painter.drawPath(path)
        painter.setBrush(QBrush(QColor(160, 20, 20), Qt.BrushStyle.SolidPattern))
        path = QPainterPath()
        path.moveTo(30, 0)
        path.lineTo(-30, -15)
        path.lineTo(-10, 0)
        path.lineTo(-30, 15)
        path.closeSubpath()
        painter.drawPath(path)
        painter.end()
        self.angle += 1
        if self.angle == 360:
            self.angle = 0
        return super().paintEvent(event)

    def closeWindow(self) -> None:
        print("closing window ...")
        self.close()


if __name__ == "__main__":
    App = QApplication(argv)
    window = Window()
    exit(App.exec())

how should I do this purpose?

I want to detect collisions between some slices of a circle and rectangles.


Solution

  • Your code has many issues, but your main problem is caused by two factors:

    1. you are using contains(), which "Returns true if the given rectangle is inside the path"; you should use intersects() instead;
    2. the "barrier" is created using the final, absolute coordinates, while all the other paths are in relative coordinates, since you are translating the painter;
    3. the painter is rotated, not the objects;

    The result is that even using contains(), it will never work, since the objects are too far apart:

    • barrier = QRectF(1920//2-25, 1080//2-25-40, 50, 20) which is 935, 475, 50, 20;
    • rect = QRectF(-r/2, -r/2, r, r) which is -100, -100, 200, 200;

    As you can see, they are not even close.

    Not only: it wouldn't work anyway because the paths never consider the rotation, as you applied it to the painter.

    The proper solution requires to:

    • always use the same coordinate reference;
    • apply the transformations (translation and rotation) to the objects and then detect the possible collision;

    In order to do that, we can use Qtransform along with its map() function, which returns a new transformed path.

    Note that QTransform is always aligned to the 0, 0 coordinate, so in order to properly rotate around a different reference point, you must:

    1. translate the transform to the reference point;
    2. apply the rotation;
    3. translate back using the negative of the reference point;

    Here is an improved version of your code:

        def paintEvent(self, event: QPaintEvent) -> super:
            secs, ms = divmod(self.clockCounterVariable, 100)
            mins, secs = divmod(secs, 60)
            hours, mins = divmod(mins, 60)
    
            painter = QPainter(self)
            painter.setPen(QPen(QColor(255, 128, 20),  1, Qt.SolidLine))
            painter.drawText(QRectF(35, 30, 400, 30), Qt.AlignLeft, 
                "{:02d} : {:02d} : {:02d} : {:02d}".format(hours, mins, secs, ms))
            painter.setPen(QPen(QColor(20, 20, 20),  -1, Qt.SolidLine))
            painter.setBrush(QBrush(QColor(20, 20, 160), Qt.SolidPattern))
    
            
            reference = QPointF(1920 / 2, 1080 / 2)
            barrier = QRectF(reference.x() - 25, reference.y() - 65, 50, 20)
            painter.drawRect(barrier)
    
            r = 200
            a = 40
            b = a * 2
            rect = QRectF(-r/2, -r/2, r, r).translated(reference)
    
            collideBrush = QBrush(QColor(200, 20, 20, 50))
            normalBrush = QBrush(QColor(20, 200, 20, 50))
    
            reference = rect.center()
            transform = QTransform()
            transform.translate(reference.x(), reference.y())
            transform.rotate(self.angle)
            transform.translate(-reference.x(), -reference.y())
    
            for deltaAngle in (0, 90, -90):
                path = QPainterPath(reference)
                path.arcTo(rect, -a + deltaAngle, b)
                path.closeSubpath()
                path = transform.map(path)
                if path.intersects(barrier):
                    painter.setBrush(collideBrush)
                else:
                    painter.setBrush(normalBrush)
                painter.drawPath(path)
    
            painter.setBrush(QBrush(QColor(160, 20, 20)))
            path = QPainterPath()
            path.moveTo(30, 0)
            path.lineTo(-30, -15)
            path.lineTo(-10, 0)
            path.lineTo(-30, 15)
            path.closeSubpath()
            path.translate(rect.center())
            painter.drawPath(transform.map(path))
    
            self.angle = (self.angle + 1) % 360
    

    Note that there are other issues with your code, for instance:

    • use event.key(), not QKeyEvent.key(event);
    • I don't know where you got those 112 and 115 values for the keys, but they are wrong: event.key() returns an enum that is always the same for letter keys, no matter the modifiers (which you must check with event.modifier()); just check the key against Qt.Key.Key_P or Qt.Key.Key_S and it will work for both lower and upper cases;
    • event handlers don't return anything, those return when calling the base implementations are useless;
    • you are creating instance attributes that are only used within the scope of a function: setting self.milliseconds, self.seconds etc. is quite pointless for this case; if you do need those values outside of that function, they should certainly not be computed in the paintEvent(), because if the window is hidden it will not be called: instead, compute those values in clockCounter and call self.update();

    Finally, when dealing with complex graphics, implementing everything with the basic QPainter is not really effective, and normally results in making things more complex (and prone to errors and bugs) than necessary. Consider using the Graphics View Framework instead.