Bond graph ports and drawing added

This commit is contained in:
2026-07-22 12:24:58 +02:00
parent 0c578af85d
commit 09248421d0
12 changed files with 614 additions and 165 deletions

View File

@@ -1,8 +1,12 @@
from dataclasses import dataclass
from typing import Literal
from PySide6.QtCore import Qt
ArrowStyle = Literal["open", "half", "filled"]
@dataclass(frozen=True)
class ConnectionStyle:
color: str = "#285f9e"
@@ -13,11 +17,17 @@ class ConnectionStyle:
arrow_at_source: bool = False
arrow_at_target: bool = True
arrow_size: float = 10.0
arrow_style: ArrowStyle = "filled"
def __post_init__(self) -> None:
if self.arrow_style not in {"open", "half", "filled"}:
raise ValueError(f"Unknown connection arrow style: {self.arrow_style}")
# This is the intentional code-level styling point for every port/connection type.
CONNECTION_STYLES: dict[str, ConnectionStyle] = {
"signal": ConnectionStyle(),
"power": ConnectionStyle(width=3.0, arrow_style="half", color="#000000"),
}

View File

@@ -1,6 +1,6 @@
from copy import deepcopy
from PySide6.QtCore import QPointF, QRectF, QSizeF, Qt
from PySide6.QtCore import QPointF, QRectF, QSizeF, Qt, QTimer
from PySide6.QtGui import QColor, QPainter, QPainterPath, QPen, QPolygonF
from PySide6.QtWidgets import (
QColorDialog,
@@ -487,7 +487,13 @@ class IconEditorDialog(QDialog):
self.scene.addItem(ShapeItem(element))
self._add_ports(self.inputs, "input", 0.0)
self._add_ports(self.outputs, "output", self.icon.width)
self.view.center_icon()
self._initial_fit_pending = True
def showEvent(self, event) -> None: # noqa: N802 (Qt API name)
super().showEvent(event)
if self._initial_fit_pending:
self._initial_fit_pending = False
QTimer.singleShot(0, self.view.center_icon)
def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None:
spacing = self.icon.height / (len(ports) + 1)

View File

@@ -485,23 +485,39 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
handle.setVisible(self.isSelected())
@staticmethod
def _arrow(end: QPointF, direction: QPointF, size: float) -> QPolygonF:
def _arrow_points(
end: QPointF, direction: QPointF, size: float
) -> tuple[QPointF, QPointF, QPointF]:
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5)
unit = QPointF(direction.x() / length, direction.y() / length)
normal = QPointF(-unit.y(), unit.x())
base = end - unit * size
return QPolygonF([end, base + normal * size * 0.45, base - normal * size * 0.45])
return end, base + normal * size * 0.45, base - normal * size * 0.45
def _draw_arrow(
self, painter: QPainter, end: QPointF, direction: QPointF
) -> None:
tip, left, right = self._arrow_points(
end, direction, self.style.arrow_size
)
color = self.pen().color()
if self.style.arrow_style == "filled":
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(color)
painter.drawPolygon(QPolygonF([tip, left, right]))
return
painter.setPen(QPen(color, self.pen().widthF(), Qt.PenStyle.SolidLine))
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawLine(tip, left)
if self.style.arrow_style == "open":
painter.drawLine(tip, right)
def paint(self, painter: QPainter, option, widget=None) -> None:
super().paint(painter, option, widget)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(self.pen().color())
if self.style.arrow_at_target and not self.target_is_junction:
painter.drawPolygon(self._arrow(self.end, self.end_direction, self.style.arrow_size))
self._draw_arrow(painter, self.end, self.end_direction)
if self.style.arrow_at_source and not self.source_is_junction:
painter.drawPolygon(
self._arrow(self.start, -self.start_direction, self.style.arrow_size)
)
self._draw_arrow(painter, self.start, -self.start_direction)
class WaypointHandle(QGraphicsEllipseItem):
@@ -1207,9 +1223,22 @@ class GraphScene(QGraphicsScene):
def add_pairs(
source_item: ComponentGraphicsItem, target_item: ComponentGraphicsItem
) -> None:
for output in source_item.component.outputs:
source_ports = [
*source_item.component.outputs,
*(
port
for port in source_item.component.inputs
if port.orientation == "indifferent"
),
]
for output in source_ports:
for input_port in target_item.component.inputs:
if not PortTypeRegistry.compatible(output.type, input_port.type):
if not PortTypeRegistry.compatible(
output.type,
input_port.type,
output.domain,
input_port.domain,
):
continue
source = Endpoint(block=source_item.component_id, port=output.id)
target = Endpoint(block=target_item.component_id, port=input_port.id)
@@ -1240,7 +1269,9 @@ class GraphScene(QGraphicsScene):
if terminal.direction == "input":
for port in component.component.inputs:
target = Endpoint(block=component.component_id, port=port.id)
if not PortTypeRegistry.compatible(terminal.port.type, port.type):
if not PortTypeRegistry.compatible(
terminal.port.type, port.type, terminal.port.domain, port.domain
):
continue
if not self.controller.endpoint_accepts_connection(interface, "source"):
continue
@@ -1254,9 +1285,19 @@ class GraphScene(QGraphicsScene):
)
)
else:
for port in component.component.outputs:
ports = [
*component.component.outputs,
*(
port
for port in component.component.inputs
if port.orientation == "indifferent"
),
]
for port in ports:
source = Endpoint(block=component.component_id, port=port.id)
if not PortTypeRegistry.compatible(port.type, terminal.port.type):
if not PortTypeRegistry.compatible(
port.type, terminal.port.type, port.domain, terminal.port.domain
):
continue
if not self.controller.endpoint_accepts_connection(source, "source"):
continue