Start with signal lib

This commit is contained in:
Joppe Blondel
2026-08-17 18:26:41 +02:00
parent e88c58f095
commit 60a30d4ee3
12 changed files with 928 additions and 172 deletions
+30 -18
View File
@@ -5,7 +5,7 @@ from enum import Enum
from itertools import pairwise
from math import hypot
from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, QSize, QTimer, Qt, Signal
from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, QTimer, Qt, Signal
from PySide6.QtGui import QActionGroup, QBrush, QColor, QCursor, QDragEnterEvent, QDragMoveEvent, QDropEvent, QKeySequence, QMouseEvent, QPainter, QPainterPath, QPen, QShortcut, QWheelEvent
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsTextItem, QGraphicsView, QMenu, QWidget
@@ -13,7 +13,7 @@ from bedit_core.models import BondCausality, BondConnection, BondPort, Component
from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, Icon
from bedit_gui.services.component_clipboard import COMPONENTS_MIME
from bedit_gui.ui.generated.ui_graph_editor_widget import Ui_graphEditorWidget
from bedit_gui.utils.icon import get_pixmap_bounding_box, render_icon
from bedit_gui.utils.icon import get_natural_icon_size, get_pixmap_bounding_box, render_icon
GRID_SPACING = 64
SCENE_SIZE = 10000
@@ -21,14 +21,18 @@ MIN_ZOOM = 0.2
MAX_ZOOM = 4.0
ZOOM_STEP = 1.15
ZOOM_TO_FIT_PADDING = 32.0
COMPONENT_ICON_SIZE = QSize(128, 128)
COMPONENT_LABEL_FONT_SIZE = 24.0
FALLBACK_COMPONENT_SPACING = 128
CONNECTION_WIDTH = 4.0
CONNECTION_BOUNDING_BOX_SPACING = 16.0
BOND_CONNECTION_COLOR = "#000000"
SIGNAL_CONNECTION_COLOR = "#00007f"
BOND_CONNECTION_BOUNDING_BOX_SPACING = 16.0
SIGNAL_CONNECTION_BOUNDING_BOX_SPACING = 0.0
CONNECTION_STRAIGHTEN_TOLERANCE = 8.0
ARROW_LENGTH = 32.0
ARROW_HALF_WIDTH = 16.0
SIGNAL_ARROW_LENGTH = ARROW_LENGTH / 2
SIGNAL_ARROW_HALF_WIDTH = ARROW_HALF_WIDTH / 2
CAUSALITY_TICK_HALF_LENGTH = 16.0
@@ -66,8 +70,10 @@ class GraphConnectionItem(QGraphicsPathItem):
self.connection_id = connection_id
self.editor = editor
self.setPath(self._connection_path(points, half_arrow, tick_at_source))
pen = QPen(QColor("#202020"), CONNECTION_WIDTH)
color = QColor(BOND_CONNECTION_COLOR if half_arrow else SIGNAL_CONNECTION_COLOR)
pen = QPen(color, CONNECTION_WIDTH)
self.setPen(pen)
self.setBrush(QBrush(color))
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.setZValue(-1)
@@ -100,15 +106,17 @@ class GraphConnectionItem(QGraphicsPathItem):
dx = target.x() - previous.x()
dy = target.y() - previous.y()
length = hypot(dx, dy)
back_x = target.x() - ARROW_LENGTH * dx / length
back_y = target.y() - ARROW_LENGTH * dy / length
perpendicular_x = -ARROW_HALF_WIDTH * dy / length
perpendicular_y = ARROW_HALF_WIDTH * dx / length
arrow_length = ARROW_LENGTH if half_arrow else SIGNAL_ARROW_LENGTH
arrow_half_width = ARROW_HALF_WIDTH if half_arrow else SIGNAL_ARROW_HALF_WIDTH
back_x = target.x() - arrow_length * dx / length
back_y = target.y() - arrow_length * dy / length
perpendicular_x = -arrow_half_width * dy / length
perpendicular_y = arrow_half_width * dx / length
path.moveTo(target)
path.lineTo(back_x + perpendicular_x, back_y + perpendicular_y)
if not half_arrow:
path.moveTo(target)
path.lineTo(back_x - perpendicular_x, back_y - perpendicular_y)
path.closeSubpath()
if tick_at_source is not None:
GraphConnectionItem._add_causality_tick(path, points, tick_at_source)
return path
@@ -315,6 +323,7 @@ class GraphEditorWidget(QWidget):
self.scene = GraphGraphicsScene(self)
self.scene.setSceneRect(-SCENE_SIZE / 2, -SCENE_SIZE / 2, SCENE_SIZE, SCENE_SIZE)
self.ui.graphicsView.setScene(self.scene)
self.ui.graphicsView.setRenderHint(QPainter.RenderHint.Antialiasing)
self.ui.graphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.ui.graphicsView.viewport().installEventFilter(self)
self.ui.graphicsView.viewport().setAcceptDrops(True)
@@ -381,7 +390,8 @@ class GraphEditorWidget(QWidget):
positions = {component_id: graph.component_positions.get(component_id, (index * FALLBACK_COMPONENT_SPACING, 0)) for index, component_id in enumerate(component.implementation.graph.components)}
for component_id, child in component.implementation.graph.components.items():
icon = icons.get(component_id, Icon())
pixmap = render_icon(icon, child.interface.ports, COMPONENT_ICON_SIZE, render_ports=self._mode is GraphEditorMode.CONNECTION).pixmap(COMPONENT_ICON_SIZE)
icon_size = get_natural_icon_size(icon)
pixmap = render_icon(icon, child.interface.ports, icon_size, render_ports=self._mode is GraphEditorMode.CONNECTION).pixmap(icon_size)
item = GraphComponentItem(component_id, pixmap, self)
item.setOffset(-pixmap.width() / 2, -pixmap.height() / 2)
item.setPos(*positions[component_id])
@@ -435,7 +445,8 @@ class GraphEditorWidget(QWidget):
points = self._straighten_direct_connection(points)
source_bounds = self._component_bounds[source_component].translated(*source_position)
target_bounds = self._component_bounds[target_component].translated(*target_position)
points = self._clip_connection(points, source_bounds, target_bounds)
spacing = BOND_CONNECTION_BOUNDING_BOX_SPACING if isinstance(connection, BondConnection) else SIGNAL_CONNECTION_BOUNDING_BOX_SPACING
points = self._clip_connection(points, source_bounds, target_bounds, spacing)
tick_at_source = None
if isinstance(connection, BondConnection):
tick_at_source = False if connection.causality is BondCausality.EFFORT_OUT else True if connection.causality is BondCausality.FLOW_OUT else None
@@ -582,7 +593,7 @@ class GraphEditorWidget(QWidget):
return points
@staticmethod
def _clip_connection(points: list[tuple[float, float]], source_bounds: QRectF, target_bounds: QRectF) -> list[tuple[float, float]]:
def _clip_connection(points: list[tuple[float, float]], source_bounds: QRectF, target_bounds: QRectF, spacing: float) -> list[tuple[float, float]]:
source = points[0]
target = points[-1]
source_direction = next((point for point in points[1:] if point != source), None)
@@ -590,13 +601,13 @@ class GraphEditorWidget(QWidget):
if source_direction is None or target_direction is None:
return points
clipped = list(points)
clipped[0] = GraphEditorWidget._bounding_box_edge(source_bounds, source, source_direction)
clipped[-1] = GraphEditorWidget._bounding_box_edge(target_bounds, target, target_direction)
clipped[0] = GraphEditorWidget._bounding_box_edge(source_bounds, source, source_direction, spacing)
clipped[-1] = GraphEditorWidget._bounding_box_edge(target_bounds, target, target_direction, spacing)
return clipped
@staticmethod
def _bounding_box_edge(bounds: QRectF, origin: tuple[float, float], toward: tuple[float, float]) -> tuple[float, float]:
bounds = bounds.adjusted(-CONNECTION_BOUNDING_BOX_SPACING, -CONNECTION_BOUNDING_BOX_SPACING, CONNECTION_BOUNDING_BOX_SPACING, CONNECTION_BOUNDING_BOX_SPACING)
def _bounding_box_edge(bounds: QRectF, origin: tuple[float, float], toward: tuple[float, float], spacing: float) -> tuple[float, float]:
bounds = bounds.adjusted(-spacing, -spacing, spacing, spacing)
dx = toward[0] - origin[0]
dy = toward[1] - origin[1]
horizontal_scale = (bounds.right() - origin[0]) / dx if dx > 0 else (bounds.left() - origin[0]) / dx if dx < 0 else float("inf")
@@ -664,7 +675,8 @@ class GraphEditorWidget(QWidget):
self._connection_preview.setPath(QPainterPath(mouse_position))
return
bounds = self._component_bounds[self._connection_start].translated(*source)
start = self._bounding_box_edge(bounds, source, (mouse_position.x(), mouse_position.y()))
spacing = max(BOND_CONNECTION_BOUNDING_BOX_SPACING, SIGNAL_CONNECTION_BOUNDING_BOX_SPACING)
start = self._bounding_box_edge(bounds, source, (mouse_position.x(), mouse_position.y()), spacing)
path = QPainterPath(QPointF(*start))
path.lineTo(mouse_position)
self._connection_preview.setPath(path)