How to insert a vertex into a QGraphicsPolygonItem?

99 Views Asked by At

enter image description here This polygon is drawn by the QGraphicsPolygonItem and the QGraphicsPathItem. Now I want to add vertices by clicking on the polygon edge with the mouse, but I added the code to add vertices, which causes the polygon to appear deformed. please help me. the code as follow.

 class GripItem(QGraphicsPathItem):
    editing = False
    circle = QPainterPath()
    circle.addEllipse(QRectF(-6, -6, 12, 12))
    square = QPainterPath()
    square.addRect(QRectF(-10, -10, 20, 20))
    def __init__(self, annotation_item, index):
        super(GripItem, self).__init__()
        self.m_annotation_item = annotation_item
        self.m_index = index
        self.mySignal = SignalManager()
        #self.editing = True
        self.setPath(GripItem.circle)
        self.setBrush(QColor("green"))
        self.setPen(QPen(QColor("green"), 2))
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
        self.setAcceptHoverEvents(True)
        self.setZValue(11)
        self.setCursor(QCursor(Qt.PointingHandCursor))

    def hoverEnterEvent(self, event):
        self.setPath(GripItem.square)
        self.setBrush(QColor("red"))
        super(GripItem, self).hoverEnterEvent(event)

    def hoverLeaveEvent(self, event):
        self.setPath(GripItem.circle)
        self.setBrush(QColor("green"))
        super(GripItem, self).hoverLeaveEvent(event)

    def mouseReleaseEvent(self, event):
        print("release")
        self.setSelected(False)
        super(GripItem, self).mouseReleaseEvent(event)

    def itemChange(self, change, value):
        if change == QGraphicsItem.ItemPositionChange and self.isEnabled():
            self.m_annotation_item.movePoint(self.m_index, value)
        return super(GripItem, self).itemChange(change, value)

    def mousePressEvent(self, event):
        if event.button() == Qt.RightButton:
            if self.editing:
                if self.m_annotation_item is not None:
                    print("parent")
                    #self.m_annotation_item.scene().removeItem(self)
                    it = self.m_annotation_item.m_items[self.m_index]
                    self.scene().removeItem(it)
                    del it
                    self.remove_point()
                    #del self.m_annotation_item.m_points[self.m_index]
                    self.m_annotation_item.setPolygon(QPolygonF(self.m_annotation_item.m_points))
        super(GripItem, self).mousePressEvent(event)

    def remove_point(self):
        print(self.m_annotation_item.dict_points[self.m_index])
        self.m_annotation_item.m_points.clear()
        self.m_annotation_item.dict_points[self.m_index] = 0
        for key, value in self.m_annotation_item.dict_points.items():
            if  not isinstance(value, int):
                self.m_annotation_item.m_points.append(value)


class PolygonAnnotation(QGraphicsPolygonItem): 
    editing = False 

    def __init__(self, scene):
        super(PolygonAnnotation, self).__init__()
        self.parent_scene = scene
        self.m_points = []
        self.center_x = 0
        self.center_y = 0
        self.setZValue(10)
        self.setPen(QPen(QColor("green"), 2))
        self.setAcceptHoverEvents(True)

        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)

        self.setCursor(QCursor(Qt.PointingHandCursor))
        self.setAcceptHoverEvents(True)
        self.m_items = []
        self.dict_points = dict()

    def number_of_points(self):
        return len(self.m_items)

    def calculate_center(self, p):
        self.center_x += p.x()
        self.center_y += p.y()

    def addPoint(self, p):
        self.m_points.append(p)
        #print(len(self.dict_points))
        self.dict_points[len(self.dict_points)] = p        
        self.setPolygon(QPolygonF(self.m_points))
        item = GripItem(self, len(self.m_points)-1)
        self.scene().addItem(item)
        self.m_items.append(item)
        item.setPos(p)

    def removeLastPoint(self):
        if self.m_points:
            self.m_points.pop()
            self.setPolygon(QPolygonF(self.m_points))
            it = self.m_items.pop()
            if it is not None and self.scene() is not None:
                self.scene().removeItem(it)
                del it
        if self.editing and self.dict_points:
            last_key = list(self.dict_points.keys())[-1]
            self.dict_points.pop(last_key)

    def movePoint(self, i, p):
        if 0 <= i < len(self.m_points):
            self.m_points[i] = self.mapFromScene(p)
            self.setPolygon(QPolygonF(self.m_points))

    def move_item(self, index, pos):
        if 0 <= index < len(self.m_items):
            item = self.m_items[index]
            item.setEnabled(False)
            item.setPos(pos)
            item.setEnabled(True)

    def itemChange(self, change, value):
        if change == QGraphicsItem.ItemPositionHasChanged:
            for i, point in enumerate(self.m_points):
                    self.move_item(i, self.mapToScene(point))
        return super(PolygonAnnotation, self).itemChange(change, value)


    def delete_polygon(self):
        for i in range(len(self.m_items)):
                it = self.m_items.pop()
                self.scene().removeItem(it)
        self.scene().removeItem(self)
        self.m_points.clear()
        del self

    def hoverEnterEvent(self, event):
        self.setBrush(QColor(255, 0, 0, 100))
        super(PolygonAnnotation, self).hoverEnterEvent(event)

    def hoverLeaveEvent(self, event):
        self.setBrush(QBrush(Qt.NoBrush))
        super(PolygonAnnotation, self).hoverLeaveEvent(event)

    def getQPointFromDict(self, dict):
        self.m_points.clear()
        for key, value in dict.items():
            if type(value) is not int:
                self.m_points.append(value)

    # insert a vertex to the polygon.
    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        if self.parent_scene.current_instruction != Instructions.No_Instruction:
            self.parent_scene.current_instruction = Instructions.No_Instruction
            self.parent_scene.start_point = None

        if event.button() == Qt.LeftButton:
            mouse_position = event.pos()
            index = self.isEdgeClick(mouse_position)
            if index != -1:
                # Add a vertex at the mouse click position
                self.insert_point(index, self.mapToScene(mouse_position))
        return super().mousePressEvent(event)
    
    def insert_point(self, index, p):
        self.m_points.insert(index, p)
        #print(len(self.dict_points))
        self.dict_points[len(self.dict_points)] = p        
        self.setPolygon(QPolygonF(self.m_points))
        item = GripItem(self, len(self.m_points)-1)
        self.scene().addItem(item)
        self.m_items.insert(index, item)
        item.setPos(p)

    def isEdgeClick(self, mouse_position):
        # Check if the mouse click is on the edge of the polygon
        if len(self.m_points) < 2:
            return False  # Need at least 2 points to have an edge

        polygon = QPolygonF(self.m_points)

        # Iterate through the edges of the polygon and check if the click is close to any edge
        for i in range(len(self.m_points)):
            p1 = polygon.at(i)
            p2 = polygon.at((i + 1) % len(self.m_points))

            # Calculate the distance from the point to the edge
            dist = distance_point_to_line(p1, p2, mouse_position)
                    
            # You can adjust this threshold as needed
            if dist < 1.0:
                return i+1

        return -1

This is a fail result. enter image description here

please provide the right code, thanks!

1

There are 1 best solutions below

9
musicamante On

Assuming that your distance_point_to_line computation (which you never provided, by the way) is valid, your have two problems:

  • you are adding new grip items with the wrong index: the size of the polygon vertexes (GripItem(self, len(self.m_points)-1)), instead of their real index;
  • you are not updating the indexes of the remaining vertexes (any existing vertex that is after the newly inserted one), so they will eventually return an invalid order when moving them; this is also a problem whenever you remove vertexes, because you're not updating the remaining ones;

Even if you fix the above, your implementation has conceptual flaws.

First of all, you're fundamentally storing vertex data in four different "places":

  • the m_points;
  • the dict_points;
  • indirectly, with the m_items (which are placed at the same points);
  • indirectly, the polygon points;

Then, since the grip items are clearly closely related to the polygon, they should not be "external" items, but children of the polygon.

By using a more appropriate hierarchy between those items allows better management of every aspect, avoiding unnecessary complications (such as your attempt to move grip items when the polygon is moved).

I decided to completely rewrite the whole implementation (based on some previous code of mine), because fixing the above points would have been even more complex.

The idea is that every grip item is always a child of the PolygonAnnotation, which completely manages their creation, removal and position change.

There is only one list of vertexes, which is the list of grip items, only used when they are moved or when new vertexes are added or any vertex is removed. This makes it simpler to find the index of a grip item whenever it's moved (or removed) and you don't need to manually update the indexes of each item every time the vertex count changes.

Note I changed the insertion/removal behavior by using keyboard modifiers (Shift to add, Ctrl for remove), since your code didn't explain about the "editing mode", nor what that current_instruction means (you should use self.scene(), by the way).

class GripItem(QGraphicsPathItem):
    _pen = QPen(QColor('green'), 2)
    circle = QPainterPath()
    circle.addEllipse(QRectF(-6, -6, 12, 12))
    circleBrush = QBrush(QColor('green'))
    square = QPainterPath()
    square.addRect(QRectF(-10, -10, 20, 20))
    squareBrush = QBrush(QColor('red'))
    # keep the bounding rect consistent
    _boundingRect = (circle|square).boundingRect()

    def __init__(self, pos, parent):
        super().__init__(parent)
        self.poly = parent
        self.setPos(pos)

        self.setFlags(
            QGraphicsItem.ItemIsSelectable
            | QGraphicsItem.ItemIsMovable
            | QGraphicsItem.ItemSendsGeometryChanges
        )
        self.setAcceptHoverEvents(True)
        self.setCursor(QCursor(Qt.PointingHandCursor))

        self.setPen(self._pen)
        self._setHover(False)

    def itemChange(self, change, value):
        if change == QGraphicsItem.ItemPositionHasChanged:
            self.poly.gripMoved(self)
        return super().itemChange(change, value)

    def _setHover(self, hover):
        if hover:
            self.setBrush(self.squareBrush)
            self.setPath(self.square)
        else:
            self.setBrush(self.circleBrush)
            self.setPath(self.circle)

    def boundingRect(self):
        return self._boundingRect

    def hoverEnterEvent(self, event):
        super().hoverEnterEvent(event)
        self._setHover(True)

    def hoverLeaveEvent(self, event):
        super().hoverLeaveEvent(event)
        self._setHover(False)

    def mousePressEvent(self, event):
        if (
            event.button() == Qt.LeftButton
            and event.modifiers() == Qt.ControlModifier
        ):
            self.poly.removeGrip(self)
        else:
            super().mousePressEvent(event)


class PolygonAnnotation(QGraphicsPolygonItem):
    _threshold = None
    _pen = QPen(QColor("green"), 2)
    normalBrush = QBrush(Qt.NoBrush)
    hoverBrush = QBrush(QColor(255, 0, 0, 100))
    def __init__(self, *args):
        super().__init__()

        self.setFlags(
            QGraphicsItem.ItemIsSelectable
            | QGraphicsItem.ItemIsMovable
            | QGraphicsItem.ItemSendsGeometryChanges
        )
        self.setAcceptHoverEvents(True)
        self.setCursor(QCursor(Qt.PointingHandCursor))

        self.setPen(self._pen)

        self.gripItems = []

        if len(args) == 1:
            arg = args[0]
            if isinstance(arg, QPolygonF):
                self.setPolygon(arg)
            if isinstance(arg, (tuple, list)):
                args = arg
        if all(isinstance(p, QPointF) for p in args):
            self.setPolygon(QPolygonF(args))

    def threshold(self):
        if self._threshold is not None:
            return self._threshold
        return self.pen().width() or 1.

    def setThreshold(self, threshold):
        self._threshold = threshold

    def setPolygon(self, poly):
        if self.polygon() == poly:
            return
        if self.gripItems:
            scene = self.scene()
            while self.gripItems:
                grip = self.gripItems.pop()
                if scene:
                    scene.removeItem(grip)

        super().setPolygon(poly)
        for i, p in enumerate(poly):
            self.gripItems.append(GripItem(p, self))

    def addPoint(self, pos):
        self.insertPoint(len(self.gripItems), pos)

    def insertPoint(self, index, pos):
        poly = list(self.polygon())
        poly.insert(index, pos)
        self.gripItems.insert(index, GripItem(pos, self))
        # just call the base implementation, not the override, as all required
        # items are already in place
        super().setPolygon(QPolygonF(poly))

    def removePoint(self, index):
        if len(self.gripItems) <= 3:
            # a real polygon always has at least three vertexes,
            # otherwise it would be a line or a point
            return
        poly = list(self.polygon())
        poly.pop(index)
        grip = self.gripItems.pop(index)
        if self.scene():
            self.scene().removeItem(grip)
        # call the base implementation, as in insertPoint()
        super().setPolygon(QPolygonF(poly))

    def closestPointToPoly(self, pos):
        '''
            Get the position along the polygon sides that is the closest
            to the given point.
            Returns:
            - distance from the edge
            - QPointF within the polygon edge
            - insertion index
            If no closest point is found, distance and index are -1
        '''
        poly = self.polygon()
        points = list(poly)

        # iterate through pair of points, if the polygon is not "closed",
        # add the start to the end
        p1 = points.pop(0)
        if points[-1] != p1: # identical to QPolygonF.isClosed()
            points.append(p1)
        intersections = []
        for i, p2 in enumerate(points, 1):
            line = QLineF(p1, p2)
            inters = QPointF()
            # create a perpendicular line that starts at the given pos
            perp = QLineF.fromPolar(
                self.threshold(), line.angle() + 90).translated(pos)
            if line.intersects(perp, inters) != QLineF.BoundedIntersection:
                # no intersection, reverse the perpendicular line by 180°
                perp.setAngle(perp.angle() + 180)
                if line.intersects(perp, inters) != QLineF.BoundedIntersection:
                    # the pos is not within the line extent, ignore it
                    p1 = p2
                    continue
            # get the distance between the given pos and the found intersection
            # point, then add it, the intersection and the insertion index to
            # the intersection list
            intersections.append((
                QLineF(pos, inters).length(), inters, i))
            p1 = p2

        if intersections:
            # return the result with the shortest distance
            return sorted(intersections)[0]
        return -1, QPointF(), -1

    def gripMoved(self, grip):
        if grip in self.gripItems:
            poly = list(self.polygon())
            poly[self.gripItems.index(grip)] = grip.pos()
            super().setPolygon(QPolygonF(poly))

    def removeGrip(self, grip):
        if grip in self.gripItems:
            self.removePoint(self.gripItems.index(grip))

    def hoverEnterEvent(self, event):
        super().hoverEnterEvent(event)
        self.setBrush(self.hoverBrush)

    def hoverLeaveEvent(self, event):
        super().hoverLeaveEvent(event)
        self.setBrush(self.normalBrush)

    def mousePressEvent(self, event):
        if (
            event.button() == Qt.LeftButton
            and event.modifiers() == Qt.ShiftModifier
        ):
            dist, pos, index = self.closestPointToPoly(event.pos())
            if index >= 0 and dist <= self.threshold():
                self.insertPoint(index, pos)
                return
        super().mousePressEvent(event)

You can test the above with the following example code:

def randomPoly(count=8, size=None):
    if count < 3:
        count = 3
    if isinstance(size, int):
        size = max(10, size)
    else:
        size = max(50, count * 20)
    maxDiff = size / 5
    path = QPainterPath()
    path.addEllipse(QRectF(0, 0, size, size))
    mid = 2 / count
    points = []
    for i in range(count):
        point = path.pointAtPercent((i / count + mid) % 1.)
        randiff = QPointF(
            uniform(-maxDiff, maxDiff), uniform(-maxDiff, maxDiff))
        points.append(point + randiff)
    return QPolygonF(points)


app = QApplication([])
scene = QGraphicsScene()
scene.addItem(PolygonAnnotation(randomPoly(10, 200)))
view = QGraphicsView(scene)
view.resize(view.sizeHint() + QSize(250, 250))
view.setRenderHint(QPainter.Antialiasing)
view.show()
app.exec()