Start with signal lib

This commit is contained in:
2026-08-17 18:26:41 +02:00
parent e88c58f095
commit 60a30d4ee3
12 changed files with 928 additions and 172 deletions

25
icons/signal_base.json Normal file
View File

@@ -0,0 +1,25 @@
{
"shapes": {
"14409738-45af-40c5-9c9e-827218d54a95": {
"layer": 0,
"type": "rectangle",
"pos": [
-48,
-48
],
"width": 96.0,
"height": 96.0,
"line_type": "solid",
"line_thickness": 5.0,
"corner_radius": 8.0,
"line_color": "#00007fff",
"fill_color": "#ebebebff"
}
},
"port_positions": {
"d5681d1d-70ee-4623-a41a-5a572e46d42c": [
-8,
-8
]
}
}

View File

@@ -0,0 +1,55 @@
{
"shapes": {
"9ed4189a-123b-431f-a6bc-bbf169487703": {
"layer": 0,
"type": "rectangle",
"pos": [
-48,
-48
],
"width": 96.0,
"height": 96.0,
"line_type": "solid",
"line_thickness": 5.0,
"corner_radius": 8.0,
"line_color": "#00007fff",
"fill_color": "#ebebebff"
},
"ac1b7cf2-ce50-44b7-9f8f-077ca36f7369": {
"layer": 4,
"type": "line",
"pos": [
-32,
-32
],
"end": [
-32,
32
],
"line_type": "solid",
"line_thickness": 3.0,
"line_color": "#000000ff"
},
"2e623060-349e-4a48-97b0-a81921df5870": {
"layer": 2,
"type": "line",
"pos": [
-32,
32
],
"end": [
32,
32
],
"line_type": "solid",
"line_thickness": 3.0,
"line_color": "#000000ff"
}
},
"port_positions": {
"37eb6e52-6e89-4e09-8923-43c6230f5486": [
-8,
-8
]
}
}

Binary file not shown.

BIN
lib/signal.beb Normal file

Binary file not shown.

BIN
lib/signal_sources.beb Normal file

Binary file not shown.

View File

@@ -35,6 +35,7 @@ class LibraryController(QObject):
tree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
tree.setColumnWidth(1, 56)
window.ui.actionReload_Libraries.triggered.connect(self.reload)
self.reload()
def reload(self) -> None:

View File

@@ -59,6 +59,7 @@
<addaction name="actionDelete"/>
<addaction name="separator"/>
<addaction name="actionSettings"/>
<addaction name="actionReload_Libraries"/>
</widget>
<widget class="QMenu" name="menuView">
<property name="title">
@@ -351,6 +352,17 @@
<string>Settings</string>
</property>
</action>
<action name="actionReload_Libraries">
<property name="text">
<string>Reload Libraries</string>
</property>
<property name="toolTip">
<string>Reload configured libraries</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionDelete">
<property name="icon">
<iconset resource="../../resources/resources.qrc">

View File

@@ -83,6 +83,9 @@ class Ui_MainWindow(object):
self.actionToolbars.setObjectName(u"actionToolbars")
self.actionSettings = QAction(MainWindow)
self.actionSettings.setObjectName(u"actionSettings")
self.actionReload_Libraries = QAction(MainWindow)
self.actionReload_Libraries.setObjectName(u"actionReload_Libraries")
self.actionReload_Libraries.setMenuRole(QAction.MenuRole.NoRole)
self.actionDelete = QAction(MainWindow)
self.actionDelete.setObjectName(u"actionDelete")
icon7 = QIcon()
@@ -228,6 +231,7 @@ class Ui_MainWindow(object):
self.menuEdit.addAction(self.actionDelete)
self.menuEdit.addSeparator()
self.menuEdit.addAction(self.actionSettings)
self.menuEdit.addAction(self.actionReload_Libraries)
self.menuView.addAction(self.actionReset_Layout)
self.menuView.addAction(self.actionPanels)
self.menuView.addAction(self.actionToolbars)
@@ -310,6 +314,10 @@ class Ui_MainWindow(object):
self.actionPanels.setText(QCoreApplication.translate("MainWindow", u"Panels", None))
self.actionToolbars.setText(QCoreApplication.translate("MainWindow", u"Toolbars", None))
self.actionSettings.setText(QCoreApplication.translate("MainWindow", u"Settings", None))
self.actionReload_Libraries.setText(QCoreApplication.translate("MainWindow", u"Reload Libraries", None))
#if QT_CONFIG(tooltip)
self.actionReload_Libraries.setToolTip(QCoreApplication.translate("MainWindow", u"Reload configured libraries", None))
#endif // QT_CONFIG(tooltip)
self.actionDelete.setText(QCoreApplication.translate("MainWindow", u"Delete", None))
#if QT_CONFIG(tooltip)
self.actionDelete.setToolTip(QCoreApplication.translate("MainWindow", u"Delete selected", None))

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
from math import ceil
from PySide6.QtCore import QRectF, QSize, Qt
from PySide6.QtGui import QBrush, QColor, QFont, QIcon, QPainter, QPen, QPixmap, QRegion
@@ -8,6 +10,7 @@ from bedit_gui.models import Icon, Line, LineType, Rectangle, Text
PORT_SIZE = 16
DEFAULT_ICON_SIZE = QSize(48, 48)
EMPTY_NATURAL_ICON_SIZE = QSize(32, 32)
ICON_MARGIN = 4
ICON_PREVIEW_OVERSAMPLE = 4
@@ -43,6 +46,13 @@ def get_pixmap_bounding_box(pixmap: QPixmap) -> QRectF:
return QRectF(bounds) if not bounds.isEmpty() else QRectF(0, 0, pixmap.width(), pixmap.height())
def get_natural_icon_size(icon: Icon) -> QSize:
bounds = get_bounding_box(icon)
if bounds.isEmpty():
return QSize(EMPTY_NATURAL_ICON_SIZE)
return QSize(max(1, ceil(bounds.width()) + ICON_MARGIN), max(1, ceil(bounds.height()) + ICON_MARGIN))
def render_icon(icon: Icon, ports: dict[PortID, Port], size: QSize = DEFAULT_ICON_SIZE, render_ports: bool = False) -> QIcon:
pixmap = QPixmap(size)
pixmap.fill(Qt.GlobalColor.transparent)

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)

View File

@@ -11,6 +11,8 @@ from PySide6.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsPathIte
from bedit_core.models import Port, PortID, SignalDirection
from bedit_gui.models import Icon, Line, LineType, Rectangle, Shape, ShapeID, Text
ICON_SCENE_SIZE = 512
class ShapeCreationTool(Protocol):
def begin(self, position: QPointF) -> None: ...
@@ -356,7 +358,7 @@ class IconGraphicsScene(QGraphicsScene):
super().__init__(parent)
self._tool: ShapeCreationTool | None = None
self._ports: dict[PortID, Port] = {}
self.setSceneRect(-64, -64, 128, 128)
self.setSceneRect(-ICON_SCENE_SIZE / 2, -ICON_SCENE_SIZE / 2, ICON_SCENE_SIZE, ICON_SCENE_SIZE)
def set_ports(self, ports: dict[PortID, Port]) -> None:
self._ports = deepcopy(ports)

File diff suppressed because it is too large Load Diff