Ilya af66debcde feat(view): graphics scene, node/port/edge items, undoable controller
Add the QGraphicsView layer: NodeItem (SVG symbol + title + draggable,
snap-to-grid), PortItem (relation-colored anchors with compatibility
hints), EdgeItem (orthogonal routing, arc hops on crossings, line styles,
head/tail decorations, animated flow dashes), and DiagramScene binding the
document to items with edge-drawing interaction and a grid background.
Add EditorController with a QUndoStack (add/connect/delete/move/edit) and
scene interaction tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 23:20:09 +02:00

75 lines
2.5 KiB
Python

"""PortItem: a clickable, relation-colored connection anchor on a node."""
from __future__ import annotations
from PyQt6.QtCore import QPointF, QRectF, Qt
from PyQt6.QtGui import QBrush, QColor, QPen
from PyQt6.QtWidgets import QGraphicsItem
from ..model.port import Port
PORT_RADIUS = 6.0
class PortItem(QGraphicsItem):
"""Rendered anchor for a single :class:`Port`, child of its NodeItem."""
def __init__(self, node_item, port: Port) -> None:
super().__init__(node_item)
self.node_item = node_item
self.port = port
self.setAcceptHoverEvents(True)
self.setZValue(2)
self._hover = False
# Visual affordance flags set by the scene during an edge drag.
self.compatible_hint = False
self.setToolTip(f"{port.name} ({port.relation})")
self.update_position()
# -- geometry ---------------------------------------------------------
def update_position(self) -> None:
n = self.node_item.node
lx, ly = self.port.local_point(n.width, n.height)
self.setPos(lx, ly)
def boundingRect(self) -> QRectF:
r = PORT_RADIUS + 3
return QRectF(-r, -r, 2 * r, 2 * r)
def scene_center(self) -> QPointF:
return self.mapToScene(QPointF(0, 0))
def relation_color(self) -> QColor:
reg = self.node_item.scene_relations()
return QColor(reg.color(self.port.relation)) if reg else QColor("#888")
# -- interaction ------------------------------------------------------
def hoverEnterEvent(self, event):
self._hover = True
self.update()
super().hoverEnterEvent(event)
def hoverLeaveEvent(self, event):
self._hover = False
self.update()
super().hoverLeaveEvent(event)
def set_compatible_hint(self, value: bool) -> None:
if self.compatible_hint != value:
self.compatible_hint = value
self.update()
# -- painting ---------------------------------------------------------
def paint(self, painter, option, widget=None):
color = self.relation_color()
r = PORT_RADIUS
if self._hover or self.compatible_hint:
r = PORT_RADIUS + 2
painter.setRenderHint(painter.RenderHint.Antialiasing, True)
pen = QPen(color.darker(140), 1.5)
if self.compatible_hint:
pen = QPen(QColor("#000000"), 2)
painter.setPen(pen)
fill = color.lighter(120) if self._hover else color
painter.setBrush(QBrush(fill))
painter.drawEllipse(QPointF(0, 0), r, r)