Small fixes and bigger library

This commit is contained in:
2026-08-18 14:22:01 +02:00
parent 60a30d4ee3
commit 1d535c1f5b
21 changed files with 935 additions and 41 deletions

View File

@@ -0,0 +1,79 @@
{
"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"
},
"2c787197-3228-4f20-a57d-6fca410151b9": {
"layer": 5, "type": "line", "pos": [-28, -16], "end": [-24, -15], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"53e49b2b-f3d6-4aa5-bd56-7ca7fbf68bb4": {
"layer": 5, "type": "line", "pos": [-24, -15], "end": [-20, -11], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"7df5a7ce-b914-459e-93f6-f43450623baf": {
"layer": 5, "type": "line", "pos": [-20, -11], "end": [-16, -7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"dd89bdfb-f10f-467a-aa90-a534e1f30dd8": {
"layer": 5, "type": "line", "pos": [-16, -7], "end": [-12, 0], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"df9439f1-588f-46d8-873e-ecc59f5b8e36": {
"layer": 5, "type": "line", "pos": [-12, 0], "end": [-8, 7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"4984ed23-a303-494f-947d-a34964220298": {
"layer": 5, "type": "line", "pos": [-8, 7], "end": [-4, 13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"af40c4f7-d33b-426e-b130-ce78184a350f": {
"layer": 5, "type": "line", "pos": [-4, 13], "end": [0, 16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"63c0188e-4f24-4ec7-b8d6-6258ab13c226": {
"layer": 5, "type": "line", "pos": [0, 16], "end": [4, 13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"c71d5e3b-a917-4dc8-8411-2046a54e2134": {
"layer": 5, "type": "line", "pos": [4, 13], "end": [8, 7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"51e128e6-c63f-4437-b56f-d20c31d195e2": {
"layer": 5, "type": "line", "pos": [8, 7], "end": [12, 0], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"f135f861-3e98-4bb1-ab43-e51fa0eec8db": {
"layer": 5, "type": "line", "pos": [12, 0], "end": [16, -7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"422ce3eb-8d0f-4b05-bb54-126583521240": {
"layer": 5, "type": "line", "pos": [16, -7], "end": [20, -11], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"c2c60fc4-fd4c-4cfc-a90a-dd729fe551b0": {
"layer": 5, "type": "line", "pos": [20, -11], "end": [24, -15], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"0852263b-237a-4a32-a56d-91945864e01b": {
"layer": 5, "type": "line", "pos": [24, -15], "end": [28, -16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
}
},
"port_positions": {
"37eb6e52-6e89-4e09-8923-43c6230f5486": [-8, -8]
}
}

View File

@@ -0,0 +1,97 @@
{
"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"
},
"5cfcb42c-5cc3-48aa-ad92-7d19807c95a0": {
"layer": 5, "type": "line", "pos": [-28, 0], "end": [-24, -7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"78f194ed-dae0-470a-a354-f7c4c3e54c81": {
"layer": 5, "type": "line", "pos": [-24, -7], "end": [-20, -13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"cfa29bc9-5fac-4f00-bb02-52498fd14f92": {
"layer": 5, "type": "line", "pos": [-20, -13], "end": [-16, -16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"8f852157-0395-4028-a9ce-f7bd596f2976": {
"layer": 5, "type": "line", "pos": [-16, -16], "end": [-12, -16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"6f535277-a28e-4abc-82d4-4cf515a80357": {
"layer": 5, "type": "line", "pos": [-12, -16], "end": [-8, -13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"a40bba3a-d9fd-4a7c-b409-2222ce69c558": {
"layer": 5, "type": "line", "pos": [-8, -13], "end": [-4, -7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"2720ddd9-0332-46b6-bb79-dd90aa0892bd": {
"layer": 5, "type": "line", "pos": [-4, -7], "end": [0, 0], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"e420ce57-f79d-4941-8f5a-8207e15f0306": {
"layer": 5, "type": "line", "pos": [0, 0], "end": [4, 7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"f53f0310-7e14-4a87-aeb3-e079e2a8460e": {
"layer": 5, "type": "line", "pos": [4, 7], "end": [8, 13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"915cce34-b991-4119-88b5-c2dd41eec73c": {
"layer": 5, "type": "line", "pos": [8, 13], "end": [12, 16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"56d62c0f-4156-49eb-b048-b3b42304d6b0": {
"layer": 5, "type": "line", "pos": [12, 16], "end": [16, 16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"de990624-dbb2-434c-bbdf-b0ea9caa34bd": {
"layer": 5, "type": "line", "pos": [16, 16], "end": [20, 13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"b48fb633-b679-433f-8bee-f7c9549235aa": {
"layer": 5, "type": "line", "pos": [20, 13], "end": [24, 7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
},
"17f88457-c04d-4c08-90f6-f6ec5166c11c": {
"layer": 5, "type": "line", "pos": [24, 7], "end": [28, 0], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
}
},
"port_positions": {
"37eb6e52-6e89-4e09-8923-43c6230f5486": [
-8,
-8
]
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -5,40 +5,46 @@ from bedit_gui.models import Icon
class AddEmptyGraphComponent(QUndoCommand): class AddEmptyGraphComponent(QUndoCommand):
def __init__(self, document: object, parent: Component) -> None: def __init__(self, document: object, parent: Component | dict[ComponentID, Component]) -> None:
super().__init__("Add graph component") super().__init__("Add graph component")
if not isinstance(parent.implementation, GraphImplementation):
raise TypeError("parent component must have a graph implementation")
self.document = document self.document = document
self.graph = parent.implementation.graph if isinstance(parent, Component):
if not isinstance(parent.implementation, GraphImplementation):
raise TypeError("parent component must have a graph implementation")
self.components = parent.implementation.graph.components
else:
self.components = parent
self.component_id = ComponentID() self.component_id = ComponentID()
self.component = Component(name="New Graph Component", interface=Interface(), parameters={}, implementation=GraphImplementation(Graph())) self.component = Component(name="new_graph_component", interface=Interface(), parameters={}, implementation=GraphImplementation(Graph()))
def redo(self) -> None: def redo(self) -> None:
self.graph.components[self.component_id] = self.component self.components[self.component_id] = self.component
self.document.model_changed.emit(self.document.model) self.document.model_changed.emit(self.document.model)
def undo(self) -> None: def undo(self) -> None:
del self.graph.components[self.component_id] del self.components[self.component_id]
self.document.model_changed.emit(self.document.model) self.document.model_changed.emit(self.document.model)
class AddEmptyEquationComponent(QUndoCommand): class AddEmptyEquationComponent(QUndoCommand):
def __init__(self, document: object, parent: Component) -> None: def __init__(self, document: object, parent: Component | dict[ComponentID, Component]) -> None:
super().__init__("Add equation component") super().__init__("Add equation component")
if not isinstance(parent.implementation, GraphImplementation):
raise TypeError("parent component must have a graph implementation")
self.document = document self.document = document
self.graph = parent.implementation.graph if isinstance(parent, Component):
if not isinstance(parent.implementation, GraphImplementation):
raise TypeError("parent component must have a graph implementation")
self.components = parent.implementation.graph.components
else:
self.components = parent
self.component_id = ComponentID() self.component_id = ComponentID()
self.component = Component(name="New Equation Component", interface=Interface(), parameters={}, implementation=EquationImplementation()) self.component = Component(name="New Equation Component", interface=Interface(), parameters={}, implementation=EquationImplementation())
def redo(self) -> None: def redo(self) -> None:
self.graph.components[self.component_id] = self.component self.components[self.component_id] = self.component
self.document.model_changed.emit(self.document.model) self.document.model_changed.emit(self.document.model)
def undo(self) -> None: def undo(self) -> None:
del self.graph.components[self.component_id] del self.components[self.component_id]
self.document.model_changed.emit(self.document.model) self.document.model_changed.emit(self.document.model)

View File

@@ -17,7 +17,7 @@ from bedit_gui.views.main_window import MainWindow
from bedit_gui.views.models.document_tree_model import DocumentTreeModel from bedit_gui.views.models.document_tree_model import DocumentTreeModel
from bedit_gui.utils.icon import render_fitted_icon from bedit_gui.utils.icon import render_fitted_icon
ICON_SIZE = QSize(32, 32) ICON_SIZE = QSize(16, 16)
class InterfaceEditorLike(Protocol): class InterfaceEditorLike(Protocol):
def exec(self) -> int: ... def exec(self) -> int: ...
@@ -84,11 +84,11 @@ class DocumentTreeController(QObject):
window.ui.documentTree.setHeaderHidden(True) window.ui.documentTree.setHeaderHidden(True)
window.ui.documentTree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) window.ui.documentTree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
window.ui.documentTree.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) window.ui.documentTree.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
window.ui.documentTree.setIconSize(QSize(48, 48)) window.ui.documentTree.setIconSize(QSize(24, 24))
window.ui.documentTree.header().setStretchLastSection(False) window.ui.documentTree.header().setStretchLastSection(False)
window.ui.documentTree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) window.ui.documentTree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
window.ui.documentTree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) window.ui.documentTree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
window.ui.documentTree.setColumnWidth(1, 56) window.ui.documentTree.setColumnWidth(1, 28)
self._tree_viewport = window.ui.documentTree.viewport() self._tree_viewport = window.ui.documentTree.viewport()
self._tree_viewport.installEventFilter(self) self._tree_viewport.installEventFilter(self)
@@ -175,11 +175,22 @@ class DocumentTreeController(QObject):
def _show_context_menu(self, position: QPoint) -> None: def _show_context_menu(self, position: QPoint) -> None:
index = self.window.ui.documentTree.indexAt(position) index = self.window.ui.documentTree.indexAt(position)
component = self.model.value(index) value = self.model.value(index)
if not isinstance(component, Component): global_position = self.window.ui.documentTree.viewport().mapToGlobal(position)
return if isinstance(value, CoreDocument):
self._show_root_context_menu(global_position)
elif isinstance(value, Component):
self._show_component_context_menu(value, global_position)
self._show_component_context_menu(component, self.window.ui.documentTree.viewport().mapToGlobal(position)) def _show_root_context_menu(self, global_position: QPoint) -> None:
menu = QMenu(self.window.ui.documentTree)
add_graph_component = menu.addAction("Add Graph Component")
add_equation_component = menu.addAction("Add Equation Component")
selected = menu.exec(global_position)
if selected is add_graph_component:
self.document.add_empty_root_graph_component()
elif selected is add_equation_component:
self.document.add_empty_root_equation_component()
def _show_graph_component_context_menu(self, component_id: ComponentID, global_position: QPoint) -> None: def _show_graph_component_context_menu(self, component_id: ComponentID, global_position: QPoint) -> None:
component = self._components.get(component_id) component = self._components.get(component_id)

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import logging import logging
from PySide6.QtCore import QObject, Signal from PySide6.QtCore import QObject, Signal
from PySide6.QtWidgets import QAbstractItemView
from bedit_gui.services.application_logging import configure_logging from bedit_gui.services.application_logging import configure_logging
from bedit_gui.views.main_window import MainWindow from bedit_gui.views.main_window import MainWindow
@@ -49,4 +50,6 @@ class LogController(QObject):
self.emitter.message.connect(self.model.append) self.emitter.message.connect(self.model.append)
self.model.rowsInserted.connect(window.ui.listView.scrollToBottom) self.model.rowsInserted.connect(window.ui.listView.scrollToBottom)
window.ui.listView.setModel(self.model) window.ui.listView.setModel(self.model)
window.ui.listView.setWordWrap(True)
window.ui.listView.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
configure_logging(self.handler, level) configure_logging(self.handler, level)

View File

@@ -65,7 +65,7 @@ class SimulationController(QObject):
self.document.infer_causality(component) self.document.infer_causality(component)
build = self.compiler(component, build_directory) build = self.compiler(component, build_directory)
except (OSError, RuntimeError, ValueError) as exc: except (OSError, RuntimeError, ValueError) as exc:
logger.exception("Could not compile model %s", component_path) logger.error("Could not compile model %s:\n%s", component_path, exc)
QMessageBox.critical(self.window, "Could not compile", str(exc)) QMessageBox.critical(self.window, "Could not compile", str(exc))
self.window.statusBar().showMessage("Compilation failed") self.window.statusBar().showMessage("Compilation failed")
return None return None

View File

@@ -369,12 +369,22 @@ class Document(QObject):
command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name) command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name)
self.undo_stack.push(command) self.undo_stack.push(command)
def add_empty_root_graph_component(self) -> None:
command = AddEmptyGraphComponent(self, self.model.root)
command.component.name = self._unique_component_name(self.model.root, command.component.name)
self.undo_stack.push(command)
def add_empty_equation_component(self, component: Component) -> None: def add_empty_equation_component(self, component: Component) -> None:
if isinstance(component.implementation, GraphImplementation): if isinstance(component.implementation, GraphImplementation):
command = AddEmptyEquationComponent(self, component) command = AddEmptyEquationComponent(self, component)
command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name) command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name)
self.undo_stack.push(command) self.undo_stack.push(command)
def add_empty_root_equation_component(self) -> None:
command = AddEmptyEquationComponent(self, self.model.root)
command.component.name = self._unique_component_name(self.model.root, command.component.name)
self.undo_stack.push(command)
def delete_component(self, component: Component) -> None: def delete_component(self, component: Component) -> None:
self.undo_stack.push(DeleteComponent(self, component)) self.undo_stack.push(DeleteComponent(self, component))

View File

@@ -24,6 +24,8 @@ class Shape:
def from_data(cls, data: Mapping[str, Any]) -> Shape: def from_data(cls, data: Mapping[str, Any]) -> Shape:
if cls is Shape and data.get("type") == "rectangle": if cls is Shape and data.get("type") == "rectangle":
return Rectangle.from_data(data) return Rectangle.from_data(data)
if cls is Shape and data.get("type") == "ellipse":
return Ellipse.from_data(data)
if cls is Shape and data.get("type") == "text": if cls is Shape and data.get("type") == "text":
return Text.from_data(data) return Text.from_data(data)
if cls is Shape and data.get("type") == "line": if cls is Shape and data.get("type") == "line":
@@ -77,6 +79,24 @@ class Rectangle(Shape):
def to_data(self) -> dict[str, Any]: def to_data(self) -> dict[str, Any]:
return {**super().to_data(), "width": self.width, "height": self.height, "line_type": self.line_type.value, "line_thickness": self.line_thickness, "corner_radius": self.corner_radius, "line_color": self.line_color, "fill_color": self.fill_color} return {**super().to_data(), "width": self.width, "height": self.height, "line_type": self.line_type.value, "line_thickness": self.line_thickness, "corner_radius": self.corner_radius, "line_color": self.line_color, "fill_color": self.fill_color}
@dataclass
class Ellipse(Shape):
type: str = field(init=False, default="ellipse")
width: float = 32.0
height: float = 32.0
line_type: LineType = LineType.SOLID
line_thickness: float = 1.0
line_color: str = "#000000ff"
fill_color: str = "#ffffff00"
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> Ellipse:
pos = data.get("pos", [0, 0])
return cls(layer=int(data.get("layer", 0)), pos=(int(pos[0]), int(pos[1])), width=float(data.get("width", 100.0)), height=float(data.get("height", 100.0)), line_type=LineType(data.get("line_type", "solid")), line_thickness=float(data.get("line_thickness", 1.0)), line_color=str(data.get("line_color", "#000000ff")), fill_color=str(data.get("fill_color", "#ffffff00")))
def to_data(self) -> dict[str, Any]:
return {**super().to_data(), "width": self.width, "height": self.height, "line_type": self.line_type.value, "line_thickness": self.line_thickness, "line_color": self.line_color, "fill_color": self.fill_color}
@dataclass @dataclass
class Text(Shape): class Text(Shape):
type: str = field(init=False, default="text") type: str = field(init=False, default="text")

View File

@@ -30,7 +30,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>800</width> <width>800</width>
<height>22</height> <height>19</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menuEdit"> <widget class="QMenu" name="menuEdit">
@@ -80,6 +80,7 @@
<bool>false</bool> <bool>false</bool>
</attribute> </attribute>
<addaction name="actionAdd_Rectangle"/> <addaction name="actionAdd_Rectangle"/>
<addaction name="actionAdd_Circle"/>
<addaction name="actionAdd_Text"/> <addaction name="actionAdd_Text"/>
<addaction name="actionAdd_Line"/> <addaction name="actionAdd_Line"/>
</widget> </widget>
@@ -236,6 +237,21 @@
<enum>QAction::MenuRole::NoRole</enum> <enum>QAction::MenuRole::NoRole</enum>
</property> </property>
</action> </action>
<action name="actionAdd_Circle">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/draw-circle.png</normaloff>:/icons/icons/draw-circle.png</iconset>
</property>
<property name="text">
<string>Add Ellipse</string>
</property>
<property name="toolTip">
<string>Add a circle</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="../../resources/resources.qrc"/> <include location="../../resources/resources.qrc"/>

View File

@@ -0,0 +1,206 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'icon_editor_window.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QCursor, QFont, QFontDatabase, QGradient,
QIcon, QImage, QKeySequence, QLinearGradient,
QPainter, QPalette, QPixmap, QRadialGradient,
QTransform)
from PySide6.QtWidgets import (QApplication, QGraphicsView, QMainWindow, QMenu,
QMenuBar, QSizePolicy, QStatusBar, QToolBar,
QVBoxLayout, QWidget)
import resources_rc
class Ui_iconEditor(object):
def setupUi(self, iconEditor):
if not iconEditor.objectName():
iconEditor.setObjectName(u"iconEditor")
iconEditor.resize(800, 600)
icon = QIcon()
icon.addFile(u":/icons/icons/draw-path.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
iconEditor.setWindowIcon(icon)
self.actionUndo = QAction(iconEditor)
self.actionUndo.setObjectName(u"actionUndo")
icon1 = QIcon()
icon1.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionUndo.setIcon(icon1)
self.actionUndo.setMenuRole(QAction.MenuRole.NoRole)
self.actionRedo = QAction(iconEditor)
self.actionRedo.setObjectName(u"actionRedo")
icon2 = QIcon()
icon2.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionRedo.setIcon(icon2)
self.actionRedo.setMenuRole(QAction.MenuRole.NoRole)
self.actionSave = QAction(iconEditor)
self.actionSave.setObjectName(u"actionSave")
icon3 = QIcon()
icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave.setIcon(icon3)
self.actionSave.setMenuRole(QAction.MenuRole.NoRole)
self.actionCancel = QAction(iconEditor)
self.actionCancel.setObjectName(u"actionCancel")
icon4 = QIcon()
icon4.addFile(u":/icons/icons/dialog-close.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionCancel.setIcon(icon4)
self.actionCancel.setMenuRole(QAction.MenuRole.NoRole)
self.actionAdd_Rectangle = QAction(iconEditor)
self.actionAdd_Rectangle.setObjectName(u"actionAdd_Rectangle")
icon5 = QIcon()
icon5.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionAdd_Rectangle.setIcon(icon5)
self.actionAdd_Rectangle.setMenuRole(QAction.MenuRole.NoRole)
self.actionAdd_Text = QAction(iconEditor)
self.actionAdd_Text.setObjectName(u"actionAdd_Text")
icon6 = QIcon()
icon6.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionAdd_Text.setIcon(icon6)
self.actionAdd_Text.setMenuRole(QAction.MenuRole.NoRole)
self.actionSave_to_File = QAction(iconEditor)
self.actionSave_to_File.setObjectName(u"actionSave_to_File")
icon7 = QIcon()
icon7.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionSave_to_File.setIcon(icon7)
self.actionSave_to_File.setMenuRole(QAction.MenuRole.NoRole)
self.actionOpen_from_File = QAction(iconEditor)
self.actionOpen_from_File.setObjectName(u"actionOpen_from_File")
icon8 = QIcon()
icon8.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen_from_File.setIcon(icon8)
self.actionOpen_from_File.setMenuRole(QAction.MenuRole.NoRole)
self.actionAdd_Line = QAction(iconEditor)
self.actionAdd_Line.setObjectName(u"actionAdd_Line")
self.actionAdd_Line.setIcon(icon)
self.actionAdd_Line.setMenuRole(QAction.MenuRole.NoRole)
self.actionAdd_Circle = QAction(iconEditor)
self.actionAdd_Circle.setObjectName(u"actionAdd_Circle")
icon9 = QIcon()
icon9.addFile(u":/icons/icons/draw-circle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionAdd_Circle.setIcon(icon9)
self.actionAdd_Circle.setMenuRole(QAction.MenuRole.NoRole)
self.centralwidget = QWidget(iconEditor)
self.centralwidget.setObjectName(u"centralwidget")
self.verticalLayout = QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName(u"verticalLayout")
self.graphicsView = QGraphicsView(self.centralwidget)
self.graphicsView.setObjectName(u"graphicsView")
self.verticalLayout.addWidget(self.graphicsView)
iconEditor.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(iconEditor)
self.menubar.setObjectName(u"menubar")
self.menubar.setGeometry(QRect(0, 0, 800, 19))
self.menuEdit = QMenu(self.menubar)
self.menuEdit.setObjectName(u"menuEdit")
self.menuFile = QMenu(self.menubar)
self.menuFile.setObjectName(u"menuFile")
iconEditor.setMenuBar(self.menubar)
self.statusbar = QStatusBar(iconEditor)
self.statusbar.setObjectName(u"statusbar")
iconEditor.setStatusBar(self.statusbar)
self.actionToolbar = QToolBar(iconEditor)
self.actionToolbar.setObjectName(u"actionToolbar")
iconEditor.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.actionToolbar)
self.iconToolbar = QToolBar(iconEditor)
self.iconToolbar.setObjectName(u"iconToolbar")
iconEditor.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.iconToolbar)
self.menubar.addAction(self.menuFile.menuAction())
self.menubar.addAction(self.menuEdit.menuAction())
self.menuEdit.addAction(self.actionUndo)
self.menuEdit.addAction(self.actionRedo)
self.menuFile.addAction(self.actionOpen_from_File)
self.menuFile.addAction(self.actionSave_to_File)
self.menuFile.addSeparator()
self.menuFile.addAction(self.actionSave)
self.menuFile.addAction(self.actionCancel)
self.actionToolbar.addAction(self.actionUndo)
self.actionToolbar.addAction(self.actionRedo)
self.actionToolbar.addAction(self.actionSave)
self.actionToolbar.addAction(self.actionCancel)
self.iconToolbar.addAction(self.actionAdd_Rectangle)
self.iconToolbar.addAction(self.actionAdd_Circle)
self.iconToolbar.addAction(self.actionAdd_Text)
self.iconToolbar.addAction(self.actionAdd_Line)
self.retranslateUi(iconEditor)
QMetaObject.connectSlotsByName(iconEditor)
# setupUi
def retranslateUi(self, iconEditor):
iconEditor.setWindowTitle(QCoreApplication.translate("iconEditor", u"MainWindow", None))
self.actionUndo.setText(QCoreApplication.translate("iconEditor", u"Undo", None))
#if QT_CONFIG(tooltip)
self.actionUndo.setToolTip(QCoreApplication.translate("iconEditor", u"Undo", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionUndo.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Z", None))
#endif // QT_CONFIG(shortcut)
self.actionRedo.setText(QCoreApplication.translate("iconEditor", u"Redo", None))
#if QT_CONFIG(tooltip)
self.actionRedo.setToolTip(QCoreApplication.translate("iconEditor", u"Redo", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionRedo.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Y", None))
#endif // QT_CONFIG(shortcut)
self.actionSave.setText(QCoreApplication.translate("iconEditor", u"Save", None))
#if QT_CONFIG(tooltip)
self.actionSave.setToolTip(QCoreApplication.translate("iconEditor", u"Save icon", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionSave.setShortcut(QCoreApplication.translate("iconEditor", u"Return", None))
#endif // QT_CONFIG(shortcut)
self.actionCancel.setText(QCoreApplication.translate("iconEditor", u"Cancel", None))
#if QT_CONFIG(tooltip)
self.actionCancel.setToolTip(QCoreApplication.translate("iconEditor", u"Cancel icon editing", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionCancel.setShortcut(QCoreApplication.translate("iconEditor", u"Shift+Esc", None))
#endif // QT_CONFIG(shortcut)
self.actionAdd_Rectangle.setText(QCoreApplication.translate("iconEditor", u"Add Rectangle", None))
#if QT_CONFIG(tooltip)
self.actionAdd_Rectangle.setToolTip(QCoreApplication.translate("iconEditor", u"Add a rectangle", None))
#endif // QT_CONFIG(tooltip)
self.actionAdd_Text.setText(QCoreApplication.translate("iconEditor", u"Add Text", None))
#if QT_CONFIG(tooltip)
self.actionAdd_Text.setToolTip(QCoreApplication.translate("iconEditor", u"Add a text field", None))
#endif // QT_CONFIG(tooltip)
self.actionSave_to_File.setText(QCoreApplication.translate("iconEditor", u"Save to File", None))
#if QT_CONFIG(tooltip)
self.actionSave_to_File.setToolTip(QCoreApplication.translate("iconEditor", u"Save icon to a file", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionSave_to_File.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Shift+S", None))
#endif // QT_CONFIG(shortcut)
self.actionOpen_from_File.setText(QCoreApplication.translate("iconEditor", u"Open from File", None))
#if QT_CONFIG(tooltip)
self.actionOpen_from_File.setToolTip(QCoreApplication.translate("iconEditor", u"Open icon from File", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(shortcut)
self.actionOpen_from_File.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Shift+O", None))
#endif // QT_CONFIG(shortcut)
self.actionAdd_Line.setText(QCoreApplication.translate("iconEditor", u"Add Line", None))
#if QT_CONFIG(tooltip)
self.actionAdd_Line.setToolTip(QCoreApplication.translate("iconEditor", u"Add a line", None))
#endif // QT_CONFIG(tooltip)
self.actionAdd_Circle.setText(QCoreApplication.translate("iconEditor", u"Add Ellipse", None))
#if QT_CONFIG(tooltip)
self.actionAdd_Circle.setToolTip(QCoreApplication.translate("iconEditor", u"Add a circle", None))
#endif // QT_CONFIG(tooltip)
self.menuEdit.setTitle(QCoreApplication.translate("iconEditor", u"Edit", None))
self.menuFile.setTitle(QCoreApplication.translate("iconEditor", u"File", None))
self.actionToolbar.setWindowTitle(QCoreApplication.translate("iconEditor", u"toolBar", None))
self.iconToolbar.setWindowTitle(QCoreApplication.translate("iconEditor", u"toolBar", None))
# retranslateUi

View File

@@ -6,7 +6,7 @@ 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
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, Text from bedit_gui.models import Ellipse, Icon, Line, LineType, Rectangle, Text
PORT_SIZE = 16 PORT_SIZE = 16
DEFAULT_ICON_SIZE = QSize(48, 48) DEFAULT_ICON_SIZE = QSize(48, 48)
@@ -21,7 +21,7 @@ def get_bounding_box(icon: Icon) -> QRectF:
for shape in icon.shapes.values(): for shape in icon.shapes.values():
x, y = shape.pos x, y = shape.pos
points.append((x, y)) points.append((x, y))
if isinstance(shape, (Rectangle, Text)): if isinstance(shape, (Rectangle, Ellipse, Text)):
points.append((x + shape.width, y + shape.height)) points.append((x + shape.width, y + shape.height))
elif isinstance(shape, Line): elif isinstance(shape, Line):
points.append(shape.end) points.append(shape.end)
@@ -72,6 +72,10 @@ def render_icon(icon: Icon, ports: dict[PortID, Port], size: QSize = DEFAULT_ICO
painter.setPen(_line_pen(shape.line_type, shape.line_thickness, shape.line_color)) painter.setPen(_line_pen(shape.line_type, shape.line_thickness, shape.line_color))
painter.setBrush(QBrush(_color(shape.fill_color))) painter.setBrush(QBrush(_color(shape.fill_color)))
painter.drawRoundedRect(QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height), shape.corner_radius, shape.corner_radius) painter.drawRoundedRect(QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height), shape.corner_radius, shape.corner_radius)
elif isinstance(shape, Ellipse):
painter.setPen(_line_pen(shape.line_type, shape.line_thickness, shape.line_color))
painter.setBrush(QBrush(_color(shape.fill_color)))
painter.drawEllipse(QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height))
elif isinstance(shape, Text): elif isinstance(shape, Text):
font = QFont() font = QFont()
font.setPixelSize(max(1, round(shape.size))) font.setPixelSize(max(1, round(shape.size)))

View File

@@ -162,7 +162,6 @@ class GraphComponentItem(QGraphicsPixmapItem):
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if event.button() == Qt.MouseButton.LeftButton and self.editor.mode is GraphEditorMode.CONNECTION: if event.button() == Qt.MouseButton.LeftButton and self.editor.mode is GraphEditorMode.CONNECTION:
self.editor.choose_connection_component(self.component_id, event.screenPos())
event.accept() event.accept()
return return
self._drag_start = QPointF(self.pos()) self._drag_start = QPointF(self.pos())
@@ -171,6 +170,8 @@ class GraphComponentItem(QGraphicsPixmapItem):
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self.editor.mode is GraphEditorMode.CONNECTION: if self.editor.mode is GraphEditorMode.CONNECTION:
if event.button() == Qt.MouseButton.LeftButton:
self.editor.choose_connection_component(self.component_id, event.screenPos())
event.accept() event.accept()
return return
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
@@ -723,7 +724,7 @@ class GraphEditorWidget(QWidget):
@staticmethod @staticmethod
def _port_available(port_id: PortID, port: Port, used_ports: set[PortID]) -> bool: def _port_available(port_id: PortID, port: Port, used_ports: set[PortID]) -> bool:
if isinstance(port, SignalPort): if isinstance(port, SignalPort):
return port.direction is SignalDirection.OUTPUT or port_id not in used_ports return port.direction is SignalDirection.OUTPUT or port.multiplicity or port_id not in used_ports
if isinstance(port, BondPort): if isinstance(port, BondPort):
return port.multiplicity or port_id not in used_ports return port.multiplicity or port_id not in used_ports
return False return False

View File

@@ -11,7 +11,7 @@ from bedit_gui.models import Icon, Shape, ShapeID
from bedit_gui.services import icon_files from bedit_gui.services import icon_files
from bedit_gui.services.application_logging import get_logger from bedit_gui.services.application_logging import get_logger
from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor
from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, LineCreationTool, RectangleCreationTool, TextCreationTool from bedit_gui.views.icon_graphics_scene import EllipseCreationTool, IconGraphicsScene, LineCreationTool, RectangleCreationTool, TextCreationTool
from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -123,6 +123,8 @@ class IconEditorWindow(QMainWindow):
self.ui.graphicsView.viewport().installEventFilter(self) self.ui.graphicsView.viewport().installEventFilter(self)
self.ui.actionAdd_Rectangle.setCheckable(True) self.ui.actionAdd_Rectangle.setCheckable(True)
self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool) self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool)
self.ui.actionAdd_Circle.setCheckable(True)
self.ui.actionAdd_Circle.triggered.connect(self._start_ellipse_tool)
self.ui.actionAdd_Text.setCheckable(True) self.ui.actionAdd_Text.setCheckable(True)
self.ui.actionAdd_Text.triggered.connect(self._start_text_tool) self.ui.actionAdd_Text.triggered.connect(self._start_text_tool)
self.ui.actionAdd_Line.setCheckable(True) self.ui.actionAdd_Line.setCheckable(True)
@@ -306,14 +308,24 @@ class IconEditorWindow(QMainWindow):
def _start_rectangle_tool(self) -> None: def _start_rectangle_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1 layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.ui.actionAdd_Circle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False) self.ui.actionAdd_Text.setChecked(False)
self.ui.actionAdd_Line.setChecked(False) self.ui.actionAdd_Line.setChecked(False)
self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer)) self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer))
self.ui.actionAdd_Rectangle.setChecked(True) self.ui.actionAdd_Rectangle.setChecked(True)
def _start_ellipse_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.ui.actionAdd_Rectangle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False)
self.ui.actionAdd_Line.setChecked(False)
self.scene.set_creation_tool(EllipseCreationTool(self.scene, layer))
self.ui.actionAdd_Circle.setChecked(True)
def _start_text_tool(self) -> None: def _start_text_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1 layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.ui.actionAdd_Rectangle.setChecked(False) self.ui.actionAdd_Rectangle.setChecked(False)
self.ui.actionAdd_Circle.setChecked(False)
self.ui.actionAdd_Line.setChecked(False) self.ui.actionAdd_Line.setChecked(False)
self.scene.set_creation_tool(TextCreationTool(self.scene, layer)) self.scene.set_creation_tool(TextCreationTool(self.scene, layer))
self.ui.actionAdd_Text.setChecked(True) self.ui.actionAdd_Text.setChecked(True)
@@ -321,6 +333,7 @@ class IconEditorWindow(QMainWindow):
def _start_line_tool(self) -> None: def _start_line_tool(self) -> None:
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1 layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
self.ui.actionAdd_Rectangle.setChecked(False) self.ui.actionAdd_Rectangle.setChecked(False)
self.ui.actionAdd_Circle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False) self.ui.actionAdd_Text.setChecked(False)
self.scene.set_creation_tool(LineCreationTool(self.scene, layer)) self.scene.set_creation_tool(LineCreationTool(self.scene, layer))
self.ui.actionAdd_Line.setChecked(True) self.ui.actionAdd_Line.setChecked(True)
@@ -328,6 +341,7 @@ class IconEditorWindow(QMainWindow):
def _tool_active_changed(self, active: bool) -> None: def _tool_active_changed(self, active: bool) -> None:
if not active: if not active:
self.ui.actionAdd_Rectangle.setChecked(False) self.ui.actionAdd_Rectangle.setChecked(False)
self.ui.actionAdd_Circle.setChecked(False)
self.ui.actionAdd_Text.setChecked(False) self.ui.actionAdd_Text.setChecked(False)
self.ui.actionAdd_Line.setChecked(False) self.ui.actionAdd_Line.setChecked(False)
cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor

View File

@@ -6,10 +6,10 @@ from typing import Protocol
from PySide6.QtCore import QLineF, QObject, QPointF, QRectF, Qt, Signal from PySide6.QtCore import QLineF, QObject, QPointF, QRectF, Qt, Signal
from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen
from PySide6.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
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 Ellipse, Icon, Line, LineType, Rectangle, Shape, ShapeID, Text
ICON_SCENE_SIZE = 512 ICON_SCENE_SIZE = 512
@@ -26,7 +26,7 @@ class RectangleCreationTool:
self.scene = scene self.scene = scene
self.layer = layer self.layer = layer
self.start: QPointF | None = None self.start: QPointF | None = None
self.preview: QGraphicsRectItem | None = None self.preview: QGraphicsRectItem | QGraphicsEllipseItem | None = None
def begin(self, position: QPointF) -> None: def begin(self, position: QPointF) -> None:
position = self._bounded(position) position = self._bounded(position)
@@ -69,6 +69,16 @@ class TextCreationTool(RectangleCreationTool):
return Text(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height(), text="Text") return Text(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height(), text="Text")
class EllipseCreationTool(RectangleCreationTool):
def begin(self, position: QPointF) -> None:
position = self._bounded(position)
self.start = position
self.preview = self.scene.addEllipse(QRectF(position, position), QPen(Qt.PenStyle.DashLine))
def create_shape(self, rect: QRectF) -> Shape:
return Ellipse(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height())
class LineCreationTool: class LineCreationTool:
def __init__(self, scene: QGraphicsScene, layer: int) -> None: def __init__(self, scene: QGraphicsScene, layer: int) -> None:
self.scene = scene self.scene = scene
@@ -109,7 +119,8 @@ class LineCreationTool:
class ShapeGraphicsItem(QGraphicsPathItem): class ShapeGraphicsItem(QGraphicsPathItem):
handle_size = 6.0 handle_size = 8.0
handle_hit_size = 16.0
def __init__(self, shape_id: ShapeID, shape: Shape, scene: IconGraphicsScene) -> None: def __init__(self, shape_id: ShapeID, shape: Shape, scene: IconGraphicsScene) -> None:
super().__init__() super().__init__()
@@ -128,13 +139,24 @@ class ShapeGraphicsItem(QGraphicsPathItem):
def resize_handles(self) -> dict[str, QRectF]: def resize_handles(self) -> dict[str, QRectF]:
return {"size": self.resize_handle_rect()} return {"size": self.resize_handle_rect()}
def resize_handle_hit_rects(self) -> dict[str, QRectF]:
size = self.handle_hit_size
return {name: QRectF(rect.center().x() - size / 2, rect.center().y() - size / 2, size, size) for name, rect in self.resize_handles().items()}
def boundingRect(self) -> QRectF: def boundingRect(self) -> QRectF:
margin = self.handle_size / 2 margin = self.handle_hit_size / 2
return super().boundingRect().adjusted(-margin, -margin, margin, margin) return super().boundingRect().adjusted(-margin, -margin, margin, margin)
def shape(self) -> QPainterPath:
path = super().shape()
if self.isSelected():
for rect in self.resize_handle_hit_rects().values():
path.addRect(rect)
return path
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
self._original_shape = self.current_shape() self._original_shape = self.current_shape()
self._resize_handle = next((name for name, rect in self.resize_handles().items() if self.isSelected() and rect.contains(event.pos())), None) self._resize_handle = next((name for name, rect in self.resize_handle_hit_rects().items() if self.isSelected() and rect.contains(event.pos())), None)
if self._resize_handle is not None: if self._resize_handle is not None:
event.accept() event.accept()
return return
@@ -226,6 +248,37 @@ class RectangleGraphicsItem(ShapeGraphicsItem):
self.setPath(path) self.setPath(path)
class EllipseGraphicsItem(ShapeGraphicsItem):
def __init__(self, shape_id: ShapeID, shape: Ellipse, scene: IconGraphicsScene) -> None:
super().__init__(shape_id, shape, scene)
self.ellipse = deepcopy(shape)
self.setPos(shape.pos[0], shape.pos[1])
self._set_size(shape.width, shape.height)
self.setPen(scene._pen(shape))
self.setBrush(QBrush(scene._color(shape.fill_color)))
self.setZValue(shape.layer)
def current_shape(self) -> Ellipse:
shape = deepcopy(self.ellipse)
shape.pos = (round(self.pos().x()), round(self.pos().y()))
rect = self.path().boundingRect()
shape.width = rect.width()
shape.height = rect.height()
return shape
def resize_to(self, position: QPointF, _handle: str) -> None:
bounds = self.icon_scene.sceneRect()
width = max(1.0, min(bounds.right(), round(position.x())) - self.pos().x())
height = max(1.0, min(bounds.bottom(), round(position.y())) - self.pos().y())
self._set_size(width, height)
def _set_size(self, width: float, height: float) -> None:
self.prepareGeometryChange()
path = QPainterPath()
path.addEllipse(QRectF(0, 0, width, height))
self.setPath(path)
class TextGraphicsItem(ShapeGraphicsItem): class TextGraphicsItem(ShapeGraphicsItem):
def __init__(self, shape_id: ShapeID, shape: Text, scene: IconGraphicsScene) -> None: def __init__(self, shape_id: ShapeID, shape: Text, scene: IconGraphicsScene) -> None:
super().__init__(shape_id, shape, scene) super().__init__(shape_id, shape, scene)
@@ -440,13 +493,15 @@ class IconGraphicsScene(QGraphicsScene):
def _add_shape_item(self, shape_id: ShapeID, shape: Shape) -> None: def _add_shape_item(self, shape_id: ShapeID, shape: Shape) -> None:
if isinstance(shape, Rectangle): if isinstance(shape, Rectangle):
self.addItem(RectangleGraphicsItem(shape_id, shape, self)) self.addItem(RectangleGraphicsItem(shape_id, shape, self))
elif isinstance(shape, Ellipse):
self.addItem(EllipseGraphicsItem(shape_id, shape, self))
elif isinstance(shape, Text): elif isinstance(shape, Text):
self.addItem(TextGraphicsItem(shape_id, shape, self)) self.addItem(TextGraphicsItem(shape_id, shape, self))
elif isinstance(shape, Line): elif isinstance(shape, Line):
self.addItem(LineGraphicsItem(shape_id, shape, self)) self.addItem(LineGraphicsItem(shape_id, shape, self))
@staticmethod @staticmethod
def _pen(shape: Rectangle) -> QPen: def _pen(shape: Rectangle | Ellipse) -> QPen:
return IconGraphicsScene._line_pen(shape.line_type, shape.line_thickness, shape.line_color) return IconGraphicsScene._line_pen(shape.line_type, shape.line_thickness, shape.line_color)
@staticmethod @staticmethod

View File

@@ -5,7 +5,7 @@ from copy import deepcopy
from PySide6.QtCore import QRectF from PySide6.QtCore import QRectF
from PySide6.QtWidgets import QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QSpinBox, QVBoxLayout, QWidget from PySide6.QtWidgets import QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QSpinBox, QVBoxLayout, QWidget
from bedit_gui.models import Line, LineType, Rectangle, Shape, Text from bedit_gui.models import Ellipse, Line, LineType, Rectangle, Shape, Text
from bedit_gui.views.color_button import ColorButton from bedit_gui.views.color_button import ColorButton
@@ -34,6 +34,8 @@ class ShapeOptionsDialog(QDialog):
if isinstance(shape, Rectangle): if isinstance(shape, Rectangle):
self._add_rectangle_fields(shape) self._add_rectangle_fields(shape)
elif isinstance(shape, Ellipse):
self._add_ellipse_fields(shape)
elif isinstance(shape, Text): elif isinstance(shape, Text):
self._add_text_fields(shape) self._add_text_fields(shape)
elif isinstance(shape, Line): elif isinstance(shape, Line):
@@ -51,6 +53,10 @@ class ShapeOptionsDialog(QDialog):
width = min(self.width.value(), self._scene_rect.right() - self.x.value()) width = min(self.width.value(), self._scene_rect.right() - self.x.value())
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value()) height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
return Rectangle(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), corner_radius=self.corner_radius.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color()) return Rectangle(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), corner_radius=self.corner_radius.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color())
if isinstance(self._shape, Ellipse):
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
return Ellipse(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color())
if isinstance(self._shape, Text): if isinstance(self._shape, Text):
width = min(self.width.value(), self._scene_rect.right() - self.x.value()) width = min(self.width.value(), self._scene_rect.right() - self.x.value())
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value()) height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
@@ -89,6 +95,29 @@ class ShapeOptionsDialog(QDialog):
self.form.addRow("Line color", self.line_color) self.form.addRow("Line color", self.line_color)
self.form.addRow("Fill color", self.fill_color) self.form.addRow("Fill color", self.fill_color)
def _add_ellipse_fields(self, shape: Ellipse) -> None:
self.width = QSpinBox()
self.width.setRange(1, round(self._scene_rect.width()))
self.width.setValue(round(shape.width))
self.height = QSpinBox()
self.height.setRange(1, round(self._scene_rect.height()))
self.height.setValue(round(shape.height))
self.line_type = QComboBox()
for line_type in LineType:
self.line_type.addItem(line_type.value.replace("_", " ").title(), line_type)
self.line_type.setCurrentIndex(self.line_type.findData(shape.line_type))
self.line_thickness = QDoubleSpinBox()
self.line_thickness.setRange(0, 1000)
self.line_thickness.setValue(shape.line_thickness)
self.line_color = ColorButton(shape.line_color)
self.fill_color = ColorButton(shape.fill_color)
self.form.addRow("Width", self.width)
self.form.addRow("Height", self.height)
self.form.addRow("Line type", self.line_type)
self.form.addRow("Line thickness", self.line_thickness)
self.form.addRow("Line color", self.line_color)
self.form.addRow("Fill color", self.fill_color)
def _add_text_fields(self, shape: Text) -> None: def _add_text_fields(self, shape: Text) -> None:
self.width = QSpinBox() self.width = QSpinBox()
self.width.setRange(1, round(self._scene_rect.width())) self.width.setRange(1, round(self._scene_rect.width()))

View File

@@ -24,6 +24,14 @@ class ProcessResult:
stdout: str stdout: str
stderr: str stderr: str
def diagnostics(self) -> str:
sections = []
if self.stdout.strip():
sections.append(f"OpenModelica output:\n{self.stdout.strip()}")
if self.stderr.strip():
sections.append(f"OpenModelica errors:\n{self.stderr.strip()}")
return "\n\n".join(sections)
class OpenModelicaError(RuntimeError): class OpenModelicaError(RuntimeError):
"""Raised when OMC cannot start or reports a failure.""" """Raised when OMC cannot start or reports a failure."""
@@ -80,7 +88,7 @@ class OpenModelicaRunner:
f"OpenModelica timed out after {error.timeout} seconds" f"OpenModelica timed out after {error.timeout} seconds"
) from error ) from error
if result.return_code != 0: if result.return_code != 0:
details = result.stderr.strip() or result.stdout.strip() details = result.diagnostics()
suffix = f": {details}" if details else "" suffix = f": {details}" if details else ""
raise OpenModelicaError( raise OpenModelicaError(
f"OpenModelica exited with status {result.return_code}{suffix}" f"OpenModelica exited with status {result.return_code}{suffix}"
@@ -128,7 +136,7 @@ class OpenModelicaRunner:
continue continue
result = ProcessResult(command=tuple(command), return_code=process.returncode, stdout=stdout, stderr=stderr) result = ProcessResult(command=tuple(command), return_code=process.returncode, stdout=stdout, stderr=stderr)
if result.return_code != 0: if result.return_code != 0:
details = result.stderr.strip() or result.stdout.strip() details = result.diagnostics()
suffix = f": {details}" if details else "" suffix = f": {details}" if details else ""
raise OpenModelicaError(f"simulation exited with status {result.return_code}{suffix}") raise OpenModelicaError(f"simulation exited with status {result.return_code}{suffix}")
return result return result

View File

@@ -27,6 +27,7 @@ from .results import SimulationResult, load_openmodelica_csv
_MODEL_FILE = "model.mo" _MODEL_FILE = "model.mo"
_RUN_SCRIPT_FILE = "run.mos" _RUN_SCRIPT_FILE = "run.mos"
_RUN_OUTPUT_FILE = "simulation-output.txt" _RUN_OUTPUT_FILE = "simulation-output.txt"
_DEFAULT_LIBRARIES = ("Modelica",)
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -386,7 +387,7 @@ class Simulation:
if windows_executable.is_file(): if windows_executable.is_file():
executable = windows_executable executable = windows_executable
else: else:
details = process.stderr.strip() or process.stdout.strip() details = process.diagnostics()
suffix = f": {details}" if details else "" suffix = f": {details}" if details else ""
raise OpenModelicaError( raise OpenModelicaError(
f"OpenModelica did not build {model_name!r}{suffix}" f"OpenModelica did not build {model_name!r}{suffix}"
@@ -692,10 +693,20 @@ def _load_model_script(
working_directory: Path, working_directory: Path,
commands: Sequence[str], commands: Sequence[str],
) -> str: ) -> str:
"""Create a script that loads ``model.mo`` before custom commands.""" """Create a script that loads default libraries and ``model.mo`` before custom commands."""
load_libraries = []
for library in _DEFAULT_LIBRARIES:
load_libraries.extend([
f"loaded := loadModel({library});",
"if not loaded then",
" print(getErrorString());",
" exit(1);",
"end if;",
])
return "\n".join( return "\n".join(
[ [
f"cd({json.dumps(str(working_directory.resolve()))});", f"cd({json.dumps(str(working_directory.resolve()))});",
*load_libraries,
f"loaded := loadFile({json.dumps(_MODEL_FILE)});", f"loaded := loadFile({json.dumps(_MODEL_FILE)});",
"if not loaded then", "if not loaded then",
" print(getErrorString());", " print(getErrorString());",

View File

@@ -607,6 +607,179 @@
"end for;" "end for;"
] ]
} }
},
"ac5011ce-4659-4bdd-900a-63a1c779652c": {
"name": "pulse",
"interface": {
"ports": {
"69974610-3bd6-42c1-a8bb-afe6086ef0f3": {
"port_type": "signal",
"name": "y",
"direction": "output",
"multiplicity": false,
"matrix_size": [
1,
1
],
"description": null,
"value_type": "real",
"quantity": null,
"unit": null
}
}
},
"parameters": {
"8e128f7a-cf97-4f27-a664-488921819395": {
"name": "amplitude",
"value": "1.0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": null
},
"5ff17831-f2f9-4833-9e4b-4facf7c28c0c": {
"name": "width",
"value": "50.0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": "Pulse width in %"
},
"b036592b-4dc0-498b-a966-7146cf9cf95b": {
"name": "offset",
"value": "0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": null
},
"df82150d-973d-4a8a-9bd5-6ce21a2e8bbb": {
"name": "period",
"value": "1.0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": null
},
"1195c93e-3309-494e-9a56-96e8fd5e330c": {
"name": "nperiod",
"value": "-1",
"value_type": "int",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": "Number of periods (<0 means infinite)"
},
"3f5f7b5c-2794-4e75-a5c8-1ef2b0ecd87d": {
"name": "startTime",
"value": "0.0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": null
}
},
"implementation": {
"implementation_type": "equation",
"declarations": [
"Integer count;",
"Modelica.Units.SI.Time T_start;",
"Modelica.Units.SI.Time T_width=period*width/100;"
],
"initial_equations": [
"initial algorithm",
"count := integer((time-startTime)/period);",
"T_start := startTime + count*period;"
],
"equations": [
"when time >= (pre(count)+1)*period + startTime then",
"\tcount = pre(count) + 1;",
"\tT_start = time;",
"end when;",
"y = offset + (if (time < startTime or nperiod == 0 or (nperiod > 0 and count >= nperiod)) then 0 else if time < T_start + T_width then amplitude else 0);"
]
}
},
"ec0599cb-b57b-4194-b166-526d5cd5382a": {
"name": "plusminus",
"interface": {
"ports": {
"23a8b215-7c2f-4ccf-b020-4d274d7a228f": {
"port_type": "signal",
"name": "plus",
"direction": "input",
"multiplicity": true,
"matrix_size": [
1,
1
],
"description": null,
"value_type": "real",
"quantity": null,
"unit": null
},
"4c4771f8-22c8-4829-a060-913470e2b2fc": {
"port_type": "signal",
"name": "minus",
"direction": "input",
"multiplicity": true,
"matrix_size": [
1,
1
],
"description": null,
"value_type": "real",
"quantity": null,
"unit": null
},
"a186a0e1-7aea-4fb6-871c-0ba70d78b8ac": {
"port_type": "signal",
"name": "y",
"direction": "output",
"multiplicity": false,
"matrix_size": [
1,
1
],
"description": null,
"value_type": "real",
"quantity": null,
"unit": null
}
}
},
"parameters": {},
"implementation": {
"implementation_type": "equation",
"declarations": [],
"initial_equations": [],
"equations": [
"y = sum(plus) - sum(minus);"
]
}
} }
}, },
"connections": { "connections": {
@@ -661,6 +834,16 @@
"connection_type": "signal", "connection_type": "signal",
"source": "a2709c23-7485-43b2-bc87-28a0f4c37c0b", "source": "a2709c23-7485-43b2-bc87-28a0f4c37c0b",
"target": "8bd1cc42-03b9-4c46-b31b-c1f46933dc9e" "target": "8bd1cc42-03b9-4c46-b31b-c1f46933dc9e"
},
"42f8d11c-8b31-476a-8e56-dd1ac3703508": {
"connection_type": "signal",
"source": "69974610-3bd6-42c1-a8bb-afe6086ef0f3",
"target": "23a8b215-7c2f-4ccf-b020-4d274d7a228f"
},
"81e6b0d7-65f2-4edc-a303-6eee78ac1972": {
"connection_type": "signal",
"source": "5807dc83-d049-4f0f-9708-d041a752fcd0",
"target": "4c4771f8-22c8-4829-a060-913470e2b2fc"
} }
} }
} }
@@ -1149,6 +1332,139 @@
-32 -32
] ]
} }
},
"ac5011ce-4659-4bdd-900a-63a1c779652c": {
"shapes": {
"7a9115fe-4e40-49d9-ab50-2f399ca1038b": {
"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"
},
"80e150dc-07d3-4d27-9fe0-eecb82fb2689": {
"layer": 4,
"type": "line",
"pos": [
-32,
-32
],
"end": [
-32,
32
],
"line_type": "solid",
"line_thickness": 3.0,
"line_color": "#000000ff"
},
"66c7fb78-2866-4f9f-9df2-95cfa4c57a2d": {
"layer": 2,
"type": "line",
"pos": [
-32,
32
],
"end": [
32,
32
],
"line_type": "solid",
"line_thickness": 3.0,
"line_color": "#000000ff"
},
"b1b1f425-eea6-48e7-a61d-f9eb221984b7": {
"layer": 3,
"type": "line",
"pos": [
-32,
14
],
"end": [
3,
14
],
"line_type": "solid",
"line_thickness": 2.0,
"line_color": "#ffaa00ff"
},
"0c0b7ec3-557d-4fb4-b8b8-b6b6a269ec55": {
"layer": 5,
"type": "line",
"pos": [
3,
14
],
"end": [
3,
-13
],
"line_type": "solid",
"line_thickness": 2.0,
"line_color": "#ffaa00ff"
},
"e5c99d8a-69db-4915-b1e7-84ac5e52de02": {
"layer": 6,
"type": "line",
"pos": [
3,
-13
],
"end": [
32,
-13
],
"line_type": "solid",
"line_thickness": 2.0,
"line_color": "#ffaa00ff"
}
},
"port_positions": {
"69974610-3bd6-42c1-a8bb-afe6086ef0f3": [
-8,
-8
]
}
},
"ec0599cb-b57b-4194-b166-526d5cd5382a": {
"shapes": {
"ecbbad0e-67e3-4d13-ba25-7660f50bc38e": {
"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": {
"23a8b215-7c2f-4ccf-b020-4d274d7a228f": [
-24,
9
],
"4c4771f8-22c8-4829-a060-913470e2b2fc": [
-24,
-26
],
"a186a0e1-7aea-4fb6-871c-0ba70d78b8ac": [
11,
-8
]
}
} }
} }
}, },
@@ -1207,8 +1523,8 @@
48 48
], ],
"2b3844e8-1875-40d0-ae4e-7a14ca217bce": [ "2b3844e8-1875-40d0-ae4e-7a14ca217bce": [
-432, -428,
236 744
], ],
"bf08f999-bfe8-493a-9ad2-9c2965e17d6d": [ "bf08f999-bfe8-493a-9ad2-9c2965e17d6d": [
-504, -504,
@@ -1233,6 +1549,14 @@
"8e8dca73-2d11-4631-9c24-5e1a4e3b2920": [ "8e8dca73-2d11-4631-9c24-5e1a4e3b2920": [
64, 64,
128 128
],
"ac5011ce-4659-4bdd-900a-63a1c779652c": [
-432,
560
],
"ec0599cb-b57b-4194-b166-526d5cd5382a": [
-120,
656
] ]
}, },
"component_labels": { "component_labels": {