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.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
tree.setColumnWidth(1, 56) tree.setColumnWidth(1, 56)
window.ui.actionReload_Libraries.triggered.connect(self.reload)
self.reload() self.reload()
def reload(self) -> None: def reload(self) -> None:

View File

@@ -59,6 +59,7 @@
<addaction name="actionDelete"/> <addaction name="actionDelete"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionSettings"/> <addaction name="actionSettings"/>
<addaction name="actionReload_Libraries"/>
</widget> </widget>
<widget class="QMenu" name="menuView"> <widget class="QMenu" name="menuView">
<property name="title"> <property name="title">
@@ -351,6 +352,17 @@
<string>Settings</string> <string>Settings</string>
</property> </property>
</action> </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"> <action name="actionDelete">
<property name="icon"> <property name="icon">
<iconset resource="../../resources/resources.qrc"> <iconset resource="../../resources/resources.qrc">

View File

@@ -83,6 +83,9 @@ class Ui_MainWindow(object):
self.actionToolbars.setObjectName(u"actionToolbars") self.actionToolbars.setObjectName(u"actionToolbars")
self.actionSettings = QAction(MainWindow) self.actionSettings = QAction(MainWindow)
self.actionSettings.setObjectName(u"actionSettings") 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 = QAction(MainWindow)
self.actionDelete.setObjectName(u"actionDelete") self.actionDelete.setObjectName(u"actionDelete")
icon7 = QIcon() icon7 = QIcon()
@@ -228,6 +231,7 @@ class Ui_MainWindow(object):
self.menuEdit.addAction(self.actionDelete) self.menuEdit.addAction(self.actionDelete)
self.menuEdit.addSeparator() self.menuEdit.addSeparator()
self.menuEdit.addAction(self.actionSettings) self.menuEdit.addAction(self.actionSettings)
self.menuEdit.addAction(self.actionReload_Libraries)
self.menuView.addAction(self.actionReset_Layout) self.menuView.addAction(self.actionReset_Layout)
self.menuView.addAction(self.actionPanels) self.menuView.addAction(self.actionPanels)
self.menuView.addAction(self.actionToolbars) self.menuView.addAction(self.actionToolbars)
@@ -310,6 +314,10 @@ class Ui_MainWindow(object):
self.actionPanels.setText(QCoreApplication.translate("MainWindow", u"Panels", None)) self.actionPanels.setText(QCoreApplication.translate("MainWindow", u"Panels", None))
self.actionToolbars.setText(QCoreApplication.translate("MainWindow", u"Toolbars", None)) self.actionToolbars.setText(QCoreApplication.translate("MainWindow", u"Toolbars", None))
self.actionSettings.setText(QCoreApplication.translate("MainWindow", u"Settings", 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)) self.actionDelete.setText(QCoreApplication.translate("MainWindow", u"Delete", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.actionDelete.setToolTip(QCoreApplication.translate("MainWindow", u"Delete selected", None)) self.actionDelete.setToolTip(QCoreApplication.translate("MainWindow", u"Delete selected", None))

View File

@@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from math import ceil
from PySide6.QtCore import QRectF, QSize, Qt from PySide6.QtCore import QRectF, QSize, Qt
from PySide6.QtGui import QBrush, QColor, QFont, QIcon, QPainter, QPen, QPixmap, QRegion 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 PORT_SIZE = 16
DEFAULT_ICON_SIZE = QSize(48, 48) DEFAULT_ICON_SIZE = QSize(48, 48)
EMPTY_NATURAL_ICON_SIZE = QSize(32, 32)
ICON_MARGIN = 4 ICON_MARGIN = 4
ICON_PREVIEW_OVERSAMPLE = 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()) 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: def render_icon(icon: Icon, ports: dict[PortID, Port], size: QSize = DEFAULT_ICON_SIZE, render_ports: bool = False) -> QIcon:
pixmap = QPixmap(size) pixmap = QPixmap(size)
pixmap.fill(Qt.GlobalColor.transparent) pixmap.fill(Qt.GlobalColor.transparent)

View File

@@ -5,7 +5,7 @@ from enum import Enum
from itertools import pairwise from itertools import pairwise
from math import hypot 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.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 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.models import Graph, GraphComponentLabel, GraphConnection, Icon
from bedit_gui.services.component_clipboard import COMPONENTS_MIME from bedit_gui.services.component_clipboard import COMPONENTS_MIME
from bedit_gui.ui.generated.ui_graph_editor_widget import Ui_graphEditorWidget 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 GRID_SPACING = 64
SCENE_SIZE = 10000 SCENE_SIZE = 10000
@@ -21,14 +21,18 @@ MIN_ZOOM = 0.2
MAX_ZOOM = 4.0 MAX_ZOOM = 4.0
ZOOM_STEP = 1.15 ZOOM_STEP = 1.15
ZOOM_TO_FIT_PADDING = 32.0 ZOOM_TO_FIT_PADDING = 32.0
COMPONENT_ICON_SIZE = QSize(128, 128)
COMPONENT_LABEL_FONT_SIZE = 24.0 COMPONENT_LABEL_FONT_SIZE = 24.0
FALLBACK_COMPONENT_SPACING = 128 FALLBACK_COMPONENT_SPACING = 128
CONNECTION_WIDTH = 4.0 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 CONNECTION_STRAIGHTEN_TOLERANCE = 8.0
ARROW_LENGTH = 32.0 ARROW_LENGTH = 32.0
ARROW_HALF_WIDTH = 16.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 CAUSALITY_TICK_HALF_LENGTH = 16.0
@@ -66,8 +70,10 @@ class GraphConnectionItem(QGraphicsPathItem):
self.connection_id = connection_id self.connection_id = connection_id
self.editor = editor self.editor = editor
self.setPath(self._connection_path(points, half_arrow, tick_at_source)) 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.setPen(pen)
self.setBrush(QBrush(color))
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.setZValue(-1) self.setZValue(-1)
@@ -100,15 +106,17 @@ class GraphConnectionItem(QGraphicsPathItem):
dx = target.x() - previous.x() dx = target.x() - previous.x()
dy = target.y() - previous.y() dy = target.y() - previous.y()
length = hypot(dx, dy) length = hypot(dx, dy)
back_x = target.x() - ARROW_LENGTH * dx / length arrow_length = ARROW_LENGTH if half_arrow else SIGNAL_ARROW_LENGTH
back_y = target.y() - ARROW_LENGTH * dy / length arrow_half_width = ARROW_HALF_WIDTH if half_arrow else SIGNAL_ARROW_HALF_WIDTH
perpendicular_x = -ARROW_HALF_WIDTH * dy / length back_x = target.x() - arrow_length * dx / length
perpendicular_y = ARROW_HALF_WIDTH * 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.moveTo(target)
path.lineTo(back_x + perpendicular_x, back_y + perpendicular_y) path.lineTo(back_x + perpendicular_x, back_y + perpendicular_y)
if not half_arrow: if not half_arrow:
path.moveTo(target)
path.lineTo(back_x - perpendicular_x, back_y - perpendicular_y) path.lineTo(back_x - perpendicular_x, back_y - perpendicular_y)
path.closeSubpath()
if tick_at_source is not None: if tick_at_source is not None:
GraphConnectionItem._add_causality_tick(path, points, tick_at_source) GraphConnectionItem._add_causality_tick(path, points, tick_at_source)
return path return path
@@ -315,6 +323,7 @@ class GraphEditorWidget(QWidget):
self.scene = GraphGraphicsScene(self) self.scene = GraphGraphicsScene(self)
self.scene.setSceneRect(-SCENE_SIZE / 2, -SCENE_SIZE / 2, SCENE_SIZE, SCENE_SIZE) self.scene.setSceneRect(-SCENE_SIZE / 2, -SCENE_SIZE / 2, SCENE_SIZE, SCENE_SIZE)
self.ui.graphicsView.setScene(self.scene) self.ui.graphicsView.setScene(self.scene)
self.ui.graphicsView.setRenderHint(QPainter.RenderHint.Antialiasing)
self.ui.graphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) self.ui.graphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.ui.graphicsView.viewport().installEventFilter(self) self.ui.graphicsView.viewport().installEventFilter(self)
self.ui.graphicsView.viewport().setAcceptDrops(True) 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)} 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(): for component_id, child in component.implementation.graph.components.items():
icon = icons.get(component_id, Icon()) 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 = GraphComponentItem(component_id, pixmap, self)
item.setOffset(-pixmap.width() / 2, -pixmap.height() / 2) item.setOffset(-pixmap.width() / 2, -pixmap.height() / 2)
item.setPos(*positions[component_id]) item.setPos(*positions[component_id])
@@ -435,7 +445,8 @@ class GraphEditorWidget(QWidget):
points = self._straighten_direct_connection(points) points = self._straighten_direct_connection(points)
source_bounds = self._component_bounds[source_component].translated(*source_position) source_bounds = self._component_bounds[source_component].translated(*source_position)
target_bounds = self._component_bounds[target_component].translated(*target_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 tick_at_source = None
if isinstance(connection, BondConnection): 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 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 return points
@staticmethod @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] source = points[0]
target = points[-1] target = points[-1]
source_direction = next((point for point in points[1:] if point != source), None) 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: if source_direction is None or target_direction is None:
return points return points
clipped = list(points) clipped = list(points)
clipped[0] = GraphEditorWidget._bounding_box_edge(source_bounds, source, source_direction) clipped[0] = GraphEditorWidget._bounding_box_edge(source_bounds, source, source_direction, spacing)
clipped[-1] = GraphEditorWidget._bounding_box_edge(target_bounds, target, target_direction) clipped[-1] = GraphEditorWidget._bounding_box_edge(target_bounds, target, target_direction, spacing)
return clipped return clipped
@staticmethod @staticmethod
def _bounding_box_edge(bounds: QRectF, origin: tuple[float, float], toward: tuple[float, float]) -> tuple[float, float]: def _bounding_box_edge(bounds: QRectF, origin: tuple[float, float], toward: tuple[float, float], spacing: float) -> tuple[float, float]:
bounds = bounds.adjusted(-CONNECTION_BOUNDING_BOX_SPACING, -CONNECTION_BOUNDING_BOX_SPACING, CONNECTION_BOUNDING_BOX_SPACING, CONNECTION_BOUNDING_BOX_SPACING) bounds = bounds.adjusted(-spacing, -spacing, spacing, spacing)
dx = toward[0] - origin[0] dx = toward[0] - origin[0]
dy = toward[1] - origin[1] 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") 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)) self._connection_preview.setPath(QPainterPath(mouse_position))
return return
bounds = self._component_bounds[self._connection_start].translated(*source) 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 = QPainterPath(QPointF(*start))
path.lineTo(mouse_position) path.lineTo(mouse_position)
self._connection_preview.setPath(path) 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_core.models import Port, PortID, SignalDirection
from bedit_gui.models import Icon, Line, LineType, Rectangle, Shape, ShapeID, Text from bedit_gui.models import Icon, Line, LineType, Rectangle, Shape, ShapeID, Text
ICON_SCENE_SIZE = 512
class ShapeCreationTool(Protocol): class ShapeCreationTool(Protocol):
def begin(self, position: QPointF) -> None: ... def begin(self, position: QPointF) -> None: ...
@@ -356,7 +358,7 @@ class IconGraphicsScene(QGraphicsScene):
super().__init__(parent) super().__init__(parent)
self._tool: ShapeCreationTool | None = None self._tool: ShapeCreationTool | None = None
self._ports: dict[PortID, Port] = {} 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: def set_ports(self, ports: dict[PortID, Port]) -> None:
self._ports = deepcopy(ports) self._ports = deepcopy(ports)

File diff suppressed because it is too large Load Diff