Compare commits

...

2 Commits

Author SHA1 Message Date
16d9cc7651 More fixes and extended lib 2026-08-18 15:44:24 +02:00
1d535c1f5b Small fixes and bigger library 2026-08-18 14:22:01 +02:00
32 changed files with 1948 additions and 98 deletions

BIN
examples/BondGraphs.beb Normal file

Binary file not shown.

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

@@ -1,44 +1,52 @@
from copy import deepcopy
from PySide6.QtGui import QUndoCommand from PySide6.QtGui import QUndoCommand
from bedit_core.models import Component, ComponentID, Connection, ConnectionID, EquationImplementation, Graph, GraphImplementation, Interface from bedit_core.models import Component, ComponentID, Connection, ConnectionID, EquationImplementation, Graph, GraphImplementation, Interface, PortID
from bedit_gui.models import Icon from bedit_gui.models import Icon, PortMetadata
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)
@@ -56,10 +64,16 @@ class DeleteComponent(QUndoCommand):
if connection.source in port_ids or connection.target in port_ids: if connection.source in port_ids or connection.target in port_ids:
self.connections.append((index, connection_id, connection)) self.connections.append((index, connection_id, connection))
self.icons = {} self.icons = {}
self.port_metadata = {}
for component_id in self._component_ids(self.component_id, component): for component_id in self._component_ids(self.component_id, component):
icon = document.stored_component_icon(component_id) icon = document.stored_component_icon(component_id)
if icon is not None: if icon is not None:
self.icons[component_id] = icon self.icons[component_id] = icon
database = document._port_metadata_database(False)
if database is not None:
for port_id in self._port_ids(component):
if port_id in database.ports:
self.port_metadata[port_id] = deepcopy(database.ports[port_id])
def redo(self) -> None: def redo(self) -> None:
if self.parent_graph is not None: if self.parent_graph is not None:
@@ -69,6 +83,11 @@ class DeleteComponent(QUndoCommand):
self.document.model_changed.emit(self.document.model) self.document.model_changed.emit(self.document.model)
for component_id in self.icons: for component_id in self.icons:
self.document._set_component_icon(component_id, None) self.document._set_component_icon(component_id, None)
if self.port_metadata:
database = self.document.port_metadata_database()
for port_id in self.port_metadata:
database.ports.pop(port_id, None)
self.document._set_port_metadata_database(database)
def undo(self) -> None: def undo(self) -> None:
self._restore_item(self.components, self.component_id, self.component, self.component_index) self._restore_item(self.components, self.component_id, self.component, self.component_index)
@@ -78,6 +97,10 @@ class DeleteComponent(QUndoCommand):
self.document.model_changed.emit(self.document.model) self.document.model_changed.emit(self.document.model)
for component_id, icon in self.icons.items(): for component_id, icon in self.icons.items():
self.document._set_component_icon(component_id, icon) self.document._set_component_icon(component_id, icon)
if self.port_metadata:
database = self.document.port_metadata_database()
database.ports.update(deepcopy(self.port_metadata))
self.document._set_port_metadata_database(database)
@classmethod @classmethod
def _find_component(cls, components: dict[ComponentID, Component], target: Component, parent_graph: Graph | None = None) -> tuple[dict[ComponentID, Component], ComponentID, Graph | None]: def _find_component(cls, components: dict[ComponentID, Component], target: Component, parent_graph: Graph | None = None) -> tuple[dict[ComponentID, Component], ComponentID, Graph | None]:
@@ -99,6 +122,14 @@ class DeleteComponent(QUndoCommand):
component_ids.extend(cls._component_ids(child_id, child)) component_ids.extend(cls._component_ids(child_id, child))
return component_ids return component_ids
@classmethod
def _port_ids(cls, component: Component) -> list[PortID]:
port_ids = list(component.interface.ports)
if isinstance(component.implementation, GraphImplementation):
for child in component.implementation.graph.components.values():
port_ids.extend(cls._port_ids(child))
return port_ids
@staticmethod @staticmethod
def _restore_item(items: dict, item_id: object, item: object, index: int) -> None: def _restore_item(items: dict, item_id: object, item: object, index: int) -> None:
values = list(items.items()) values = list(items.items())
@@ -108,7 +139,7 @@ class DeleteComponent(QUndoCommand):
class PasteComponents(QUndoCommand): class PasteComponents(QUndoCommand):
def __init__(self, document: object, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], graph_id: ComponentID | None = None, positions: dict[ComponentID, tuple[int, int]] | None = None) -> None: def __init__(self, document: object, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], graph_id: ComponentID | None = None, positions: dict[ComponentID, tuple[int, int]] | None = None, port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
super().__init__("Paste components") super().__init__("Paste components")
self.document = document self.document = document
self.target = target self.target = target
@@ -116,6 +147,7 @@ class PasteComponents(QUndoCommand):
self.icons = icons self.icons = icons
self.graph_id = graph_id self.graph_id = graph_id
self.positions = positions or {} self.positions = positions or {}
self.port_metadata = deepcopy(port_metadata or {})
def redo(self) -> None: def redo(self) -> None:
self.target.update(self.components) self.target.update(self.components)
@@ -125,6 +157,10 @@ class PasteComponents(QUndoCommand):
self.document.model_changed.emit(self.document.model) self.document.model_changed.emit(self.document.model)
for component_id, icon in self.icons.items(): for component_id, icon in self.icons.items():
self.document._set_component_icon(component_id, icon) self.document._set_component_icon(component_id, icon)
if self.port_metadata:
database = self.document.port_metadata_database()
database.ports.update(deepcopy(self.port_metadata))
self.document._set_port_metadata_database(database)
def undo(self) -> None: def undo(self) -> None:
for component_id in self.components: for component_id in self.components:
@@ -135,3 +171,8 @@ class PasteComponents(QUndoCommand):
self.document.model_changed.emit(self.document.model) self.document.model_changed.emit(self.document.model)
for component_id in self.icons: for component_id in self.icons:
self.document._set_component_icon(component_id, None) self.document._set_component_icon(component_id, None)
if self.port_metadata:
database = self.document.port_metadata_database()
for port_id in self.port_metadata:
database.ports.pop(port_id, None)
self.document._set_port_metadata_database(database)

View File

@@ -0,0 +1,19 @@
from copy import deepcopy
from PySide6.QtGui import QUndoCommand
from bedit_gui.models import PortMetadataDatabase
class ChangePortMetadataDatabaseCommand(QUndoCommand):
def __init__(self, document: object, database: PortMetadataDatabase) -> None:
super().__init__("Change port metadata")
self.document = document
self.old_database = document.stored_port_metadata_database()
self.new_database = deepcopy(database)
def redo(self) -> None:
self.document._set_port_metadata_database(self.new_database)
def undo(self) -> None:
self.document._set_port_metadata_database(self.old_database)

View File

@@ -133,11 +133,11 @@ class DocumentTreeClipboardHandler(ClipboardHandler):
if target is None or payload is None: if target is None or payload is None:
return return
try: try:
components, icons = import_components(payload) components, icons, port_metadata = import_components(payload)
except (TypeError, ValueError) as exc: except (TypeError, ValueError) as exc:
QMessageBox.critical(self.tree, "Could not paste components", str(exc)) QMessageBox.critical(self.tree, "Could not paste components", str(exc))
return return
self.document.paste_components(target, components, icons) self.document.paste_components(target, components, icons, port_metadata)
def delete(self) -> None: def delete(self) -> None:
self.document.delete_components(self._selected_components()) self.document.delete_components(self._selected_components())
@@ -223,14 +223,14 @@ class GraphEditorClipboardHandler(ClipboardHandler):
def _paste_payload(self, graph_component: Component, payload: dict, position: tuple[int, int]) -> None: def _paste_payload(self, graph_component: Component, payload: dict, position: tuple[int, int]) -> None:
try: try:
components, icons = import_components(payload) components, icons, port_metadata = import_components(payload)
except (TypeError, ValueError) as exc: except (TypeError, ValueError) as exc:
QMessageBox.critical(self.editor, "Could not paste components", str(exc)) QMessageBox.critical(self.editor, "Could not paste components", str(exc))
return return
x, y = position x, y = position
spacing = self.editor.snap_to_grid_size * 4 spacing = self.editor.snap_to_grid_size * 4
positions = {component_id: (x + index * spacing, y + index * spacing) for index, component_id in enumerate(components)} positions = {component_id: (x + index * spacing, y + index * spacing) for index, component_id in enumerate(components)}
self.document.paste_graph_components(graph_component, components, icons, positions) self.document.paste_graph_components(graph_component, components, icons, positions, port_metadata)
def delete(self) -> None: def delete(self) -> None:
components = self._selected_components() components = self._selected_components()

View File

@@ -9,7 +9,7 @@ from PySide6.QtWidgets import QAbstractItemView, QDialog, QHeaderView, QMenu
from bedit_core.models import Component, ComponentID, ConnectionID, EquationImplementation, GraphImplementation, Port, PortID, Parameter, ParameterID from bedit_core.models import Component, ComponentID, ConnectionID, EquationImplementation, GraphImplementation, Port, PortID, Parameter, ParameterID
from bedit_core.models import Document as CoreDocument from bedit_core.models import Document as CoreDocument
from bedit_gui.documents import Document from bedit_gui.documents import Document
from bedit_gui.models import Graph, GraphComponentLabel, Icon from bedit_gui.models import Graph, GraphComponentLabel, Icon, PortMetadata
from bedit_gui.views.dialogs.interface_editor_dialog import InterfaceEditorDialog from bedit_gui.views.dialogs.interface_editor_dialog import InterfaceEditorDialog
from bedit_gui.views.dialogs.param_editor_dialog import ParamEditorDialog from bedit_gui.views.dialogs.param_editor_dialog import ParamEditorDialog
from bedit_gui.views.icon_editor_window import IconEditorWindow from bedit_gui.views.icon_editor_window import IconEditorWindow
@@ -17,11 +17,12 @@ 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: ...
def ports(self) -> dict[PortID, Port]: ... def ports(self) -> dict[PortID, Port]: ...
def port_metadata(self) -> dict[PortID, PortMetadata]: ...
class ParamEditorLike(Protocol): class ParamEditorLike(Protocol):
def exec(self) -> int: ... def exec(self) -> int: ...
@@ -29,7 +30,7 @@ class ParamEditorLike(Protocol):
InterfaceEditorFactory = Callable[ InterfaceEditorFactory = Callable[
[dict[PortID, Port], MainWindow], [dict[PortID, Port], dict[PortID, PortMetadata], MainWindow],
InterfaceEditorLike, InterfaceEditorLike,
] ]
@@ -60,15 +61,17 @@ class DocumentTreeController(QObject):
window.ui.documentTree.setModel(self.model) window.ui.documentTree.setModel(self.model)
window.ui.documentTree.selectionModel().selectionChanged.connect(self._selection_changed) window.ui.documentTree.selectionModel().selectionChanged.connect(self._selection_changed)
window.equation_editor.equation_text_change_requested.connect(self.document.update_component_equation_text) window.equation_editor.equation_text_change_requested.connect(self.document.update_component_equation_text)
window.equation_editor.port_metadata_change_requested.connect(self._change_equation_port_metadata)
document.model_changed.connect(self._on_document_changed) document.model_changed.connect(self._on_document_changed)
document.icon_changed.connect(self._on_icon_changed) document.icon_changed.connect(self._on_icon_changed)
document.port_metadata_database_changed.connect(self._on_port_metadata_database_changed)
document.graph_component_position_changed.connect(self._on_graph_component_position_changed) document.graph_component_position_changed.connect(self._on_graph_component_position_changed)
document.graph_component_label_changed.connect(self._on_graph_component_label_changed) document.graph_component_label_changed.connect(self._on_graph_component_label_changed)
document.graph_connection_points_changed.connect(self._on_graph_connection_points_changed) document.graph_connection_points_changed.connect(self._on_graph_connection_points_changed)
document.equation_text_changed.connect(self._on_equation_text_changed) document.equation_text_changed.connect(self._on_equation_text_changed)
self.model.rename_document_requested.connect(self.document.rename) self.model.rename_document_requested.connect(self.document.rename)
self.model.rename_component_requested.connect(self.document.rename_component) self.model.rename_component_requested.connect(self.document.rename_component)
window.graph_editor.component_move_requested.connect(self.document.move_graph_component) window.graph_editor.component_moves_requested.connect(self.document.move_graph_components)
window.graph_editor.component_label_move_requested.connect(self.document.move_graph_component_label) window.graph_editor.component_label_move_requested.connect(self.document.move_graph_component_label)
window.graph_editor.component_context_menu_requested.connect(self._show_graph_component_context_menu) window.graph_editor.component_context_menu_requested.connect(self._show_graph_component_context_menu)
window.graph_editor.component_open_requested.connect(self._open_graph_component) window.graph_editor.component_open_requested.connect(self._open_graph_component)
@@ -84,11 +87,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)
@@ -125,7 +128,8 @@ class DocumentTreeController(QObject):
def _show_component(self, component: Component | None) -> None: def _show_component(self, component: Component | None) -> None:
if component is not None and isinstance(component.implementation, EquationImplementation): if component is not None and isinstance(component.implementation, EquationImplementation):
self.window.equation_editor.set_component(component) port_metadata = {port_id: self.document.port_metadata(port_id) for port_id in component.interface.ports}
self.window.equation_editor.set_component(component, port_metadata)
self.window.equation_editor.show() self.window.equation_editor.show()
else: else:
self.window.equation_editor.set_component(None) self.window.equation_editor.set_component(None)
@@ -133,7 +137,8 @@ class DocumentTreeController(QObject):
if component is not None and isinstance(component.implementation, GraphImplementation): if component is not None and isinstance(component.implementation, GraphImplementation):
graph = self.document.graph_database().graphs.get(self.document.component_id(component), Graph()) graph = self.document.graph_database().graphs.get(self.document.component_id(component), Graph())
icons = {component_id: self.document.component_icon(component_id) for component_id in component.implementation.graph.components} icons = {component_id: self.document.component_icon(component_id) for component_id in component.implementation.graph.components}
self.window.graph_editor.set_component(component, graph, icons) port_metadata = {port_id: self.document.port_metadata(port_id) for child in component.implementation.graph.components.values() for port_id in child.interface.ports}
self.window.graph_editor.set_component(component, graph, icons, port_metadata)
self.window.graph_editor.show() self.window.graph_editor.show()
else: else:
self.window.graph_editor.set_component(None) self.window.graph_editor.set_component(None)
@@ -152,6 +157,17 @@ class DocumentTreeController(QObject):
if graph_component is not None and isinstance(graph_component.implementation, GraphImplementation) and component_id in graph_component.implementation.graph.components: if graph_component is not None and isinstance(graph_component.implementation, GraphImplementation) and component_id in graph_component.implementation.graph.components:
self._show_component(graph_component) self._show_component(graph_component)
def _on_port_metadata_database_changed(self, _database: object) -> None:
graph_component = self.window.graph_editor.component()
if graph_component is not None:
self._show_component(graph_component)
equation_component = self.window.equation_editor.component()
if equation_component is not None:
self.window.equation_editor.refresh_port_metadata({port_id: self.document.port_metadata(port_id) for port_id in equation_component.interface.ports})
def _change_equation_port_metadata(self, component: Component, port_metadata: dict[PortID, PortMetadata]) -> None:
self.document.update_component_ports(component, component.interface.ports, port_metadata)
def _on_graph_component_position_changed(self, graph_id: ComponentID, component_id: ComponentID, position: tuple[int, int] | None) -> None: def _on_graph_component_position_changed(self, graph_id: ComponentID, component_id: ComponentID, position: tuple[int, int] | None) -> None:
graph_component = self.window.graph_editor.component() graph_component = self.window.graph_editor.component()
if graph_component is not None and self.document.component_id(graph_component) == graph_id: if graph_component is not None and self.document.component_id(graph_component) == graph_id:
@@ -175,11 +191,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)
@@ -231,10 +258,11 @@ class DocumentTreeController(QObject):
def _edit_interface(self, component: Component) -> None: def _edit_interface(self, component: Component) -> None:
dialog = self.interface_editor_factory( dialog = self.interface_editor_factory(
component.interface.ports, component.interface.ports,
{port_id: self.document.port_metadata(port_id) for port_id in component.interface.ports},
self.window, self.window,
) )
if dialog.exec() == QDialog.DialogCode.Accepted: if dialog.exec() == QDialog.DialogCode.Accepted:
self.document.update_component_ports(component, dialog.ports()) self.document.update_component_ports(component, dialog.ports(), dialog.port_metadata())
def _edit_params(self, component: Component) -> None: def _edit_params(self, component: Component) -> None:
dialog = self.param_editor_factory( dialog = self.param_editor_factory(

View File

@@ -1,8 +1,8 @@
from PySide6.QtCore import QObject, QSize, Qt from PySide6.QtCore import QObject, QSize, Qt
from PySide6.QtWidgets import QAbstractItemView, QHeaderView from PySide6.QtWidgets import QAbstractItemView, QHeaderView
from bedit_core.models import Component, ComponentID, GraphImplementation from bedit_core.models import Component, ComponentID, GraphImplementation, PortID
from bedit_gui.models import Icon, IconDatabase from bedit_gui.models import Icon, IconDatabase, PortMetadata, PortMetadataDatabase
from bedit_gui.services.application_settings import ApplicationSettings from bedit_gui.services.application_settings import ApplicationSettings
from bedit_gui.services.component_clipboard import export_component_data from bedit_gui.services.component_clipboard import export_component_data
from bedit_gui.services.libraries import load_library_documents from bedit_gui.services.libraries import load_library_documents
@@ -18,7 +18,7 @@ class LibraryController(QObject):
super().__init__(window) super().__init__(window)
self.window = window self.window = window
self.settings = settings self.settings = settings
self._component_sources: dict[int, tuple[ComponentID, dict[ComponentID, Icon]]] = {} self._component_sources: dict[int, tuple[ComponentID, dict[ComponentID, Icon], dict[PortID, PortMetadata]]] = {}
self.model = LibraryTreeModel(self._component_payload) self.model = LibraryTreeModel(self._component_payload)
tree = window.ui.libraryTree tree = window.ui.libraryTree
@@ -44,7 +44,9 @@ class LibraryController(QObject):
for library in libraries: for library in libraries:
database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None
icons = database.icons if isinstance(database, IconDatabase) else {} icons = database.icons if isinstance(database, IconDatabase) else {}
self._collect_component_sources(library.document.root, icons) metadata_database = library.document.metadata.get("port_metadata_database") if library.document.metadata is not None else None
port_metadata = metadata_database.ports if isinstance(metadata_database, PortMetadataDatabase) else {}
self._collect_component_sources(library.document.root, icons, port_metadata)
self.model.set_documents([library.document for library in libraries]) self.model.set_documents([library.document for library in libraries])
for library in libraries: for library in libraries:
database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None
@@ -55,20 +57,22 @@ class LibraryController(QObject):
def _component_payload(self, components: list[Component]) -> dict: def _component_payload(self, components: list[Component]) -> dict:
roots = {} roots = {}
icons = {} icons = {}
port_metadata = {}
for component in components: for component in components:
source = self._component_sources.get(id(component)) source = self._component_sources.get(id(component))
if source is None: if source is None:
continue continue
component_id, source_icons = source component_id, source_icons, source_port_metadata = source
roots[component_id] = component roots[component_id] = component
icons.update(source_icons) icons.update(source_icons)
return export_component_data(roots, icons) port_metadata.update(source_port_metadata)
return export_component_data(roots, icons, port_metadata)
def _collect_component_sources(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None: def _collect_component_sources(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], port_metadata: dict[PortID, PortMetadata]) -> None:
for component_id, component in components.items(): for component_id, component in components.items():
self._component_sources[id(component)] = (component_id, icons) self._component_sources[id(component)] = (component_id, icons, port_metadata)
if isinstance(component.implementation, GraphImplementation): if isinstance(component.implementation, GraphImplementation):
self._collect_component_sources(component.implementation.graph.components, icons) self._collect_component_sources(component.implementation.graph.components, icons, port_metadata)
def _set_component_icons(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None: def _set_component_icons(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
for component_id, component in components.items(): for component_id, component in components.items():

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

@@ -17,12 +17,13 @@ from bedit_gui.commands.graph_connection_points_command import ChangeGraphConnec
from bedit_gui.commands.graph_connection_command import AddGraphConnectionCommand, DeleteGraphConnectionCommand from bedit_gui.commands.graph_connection_command import AddGraphConnectionCommand, DeleteGraphConnectionCommand
from bedit_gui.commands.graph_label_command import ChangeGraphComponentLabelCommand from bedit_gui.commands.graph_label_command import ChangeGraphComponentLabelCommand
from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand
from bedit_gui.commands.port_metadata_command import ChangePortMetadataDatabaseCommand
from bedit_gui.commands.param_commands import AddParamCommand, ChangeParamCommand, RemoveParamCommand from bedit_gui.commands.param_commands import AddParamCommand, ChangeParamCommand, RemoveParamCommand
from bedit_gui.commands.rename_component_command import RenameComponentCommand from bedit_gui.commands.rename_component_command import RenameComponentCommand
from bedit_gui.commands.rename_document_command import RenameDocumentCommand from bedit_gui.commands.rename_document_command import RenameDocumentCommand
from bedit_gui.commands.simulation_database_command import ChangeSimulationDatabaseCommand from bedit_gui.commands.simulation_database_command import ChangeSimulationDatabaseCommand
from bedit_gui.commands.component_command import AddEmptyEquationComponent, AddEmptyGraphComponent, DeleteComponent, PasteComponents from bedit_gui.commands.component_command import AddEmptyEquationComponent, AddEmptyGraphComponent, DeleteComponent, PasteComponents
from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, GraphDatabase, Icon, IconDatabase, Simulation, SimulationDatabase from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, GraphDatabase, Icon, IconDatabase, PortMetadata, PortMetadataDatabase, Simulation, SimulationDatabase
from bedit_gui.services import document_files from bedit_gui.services import document_files
@@ -35,6 +36,7 @@ class Document(QObject):
icon_changed = Signal(object, object) icon_changed = Signal(object, object)
equation_text_changed = Signal(object, str) equation_text_changed = Signal(object, str)
simulation_database_changed = Signal(object) simulation_database_changed = Signal(object)
port_metadata_database_changed = Signal(object)
graph_component_position_changed = Signal(object, object, object) graph_component_position_changed = Signal(object, object, object)
graph_component_label_changed = Signal(object, object, object) graph_component_label_changed = Signal(object, object, object)
graph_connection_points_changed = Signal(object, object, object) graph_connection_points_changed = Signal(object, object, object)
@@ -140,6 +142,40 @@ class Document(QObject):
database = self._graph_database(False) database = self._graph_database(False)
return deepcopy(database) if database is not None else GraphDatabase() return deepcopy(database) if database is not None else GraphDatabase()
def stored_port_metadata_database(self) -> PortMetadataDatabase | None:
database = self._port_metadata_database(False)
return deepcopy(database) if database is not None else None
def port_metadata_database(self) -> PortMetadataDatabase:
return self.stored_port_metadata_database() or PortMetadataDatabase()
def port_metadata(self, port_id: PortID) -> PortMetadata:
return deepcopy(self.port_metadata_database().ports.get(port_id, PortMetadata()))
def _set_port_metadata_database(self, database: PortMetadataDatabase | None) -> None:
if database is None or not database.ports:
if self.model.metadata is not None:
self.model.metadata.pop("port_metadata_database", None)
else:
if self.model.metadata is None:
self.model.metadata = {}
self.model.metadata["port_metadata_database"] = deepcopy(database)
self.port_metadata_database_changed.emit(self.stored_port_metadata_database())
def _port_metadata_database(self, create: bool) -> PortMetadataDatabase | None:
metadata = self.model.metadata
value = metadata.get("port_metadata_database") if metadata is not None else None
if isinstance(value, PortMetadataDatabase):
return value
if not create:
return None
if metadata is None:
metadata = {}
self.model.metadata = metadata
database = PortMetadataDatabase()
metadata["port_metadata_database"] = database
return database
def move_graph_component(self, graph_component: Component, component_id: ComponentID, position: tuple[int, int]) -> None: def move_graph_component(self, graph_component: Component, component_id: ComponentID, position: tuple[int, int]) -> None:
graph_id = self.component_id(graph_component) graph_id = self.component_id(graph_component)
database = self._graph_database(False) database = self._graph_database(False)
@@ -147,6 +183,18 @@ class Document(QObject):
if graph is None or graph.component_positions.get(component_id) != position: if graph is None or graph.component_positions.get(component_id) != position:
self.undo_stack.push(MoveGraphComponentCommand(self, graph_id, component_id, position)) self.undo_stack.push(MoveGraphComponentCommand(self, graph_id, component_id, position))
def move_graph_components(self, graph_component: Component, positions: dict[ComponentID, tuple[int, int]]) -> None:
graph_id = self.component_id(graph_component)
database = self._graph_database(False)
graph = database.graphs.get(graph_id) if database is not None else None
changed = {component_id: position for component_id, position in positions.items() if graph is None or graph.component_positions.get(component_id) != position}
if not changed:
return
self.undo_stack.beginMacro("Move graph components" if len(changed) > 1 else "Move graph component")
for component_id, position in changed.items():
self.undo_stack.push(MoveGraphComponentCommand(self, graph_id, component_id, position))
self.undo_stack.endMacro()
def _set_graph_component_position(self, graph_id: ComponentID, component_id: ComponentID, position: tuple[int, int] | None) -> None: def _set_graph_component_position(self, graph_id: ComponentID, component_id: ComponentID, position: tuple[int, int] | None) -> None:
if position is None: if position is None:
database = self._graph_database(False) database = self._graph_database(False)
@@ -309,7 +357,7 @@ class Document(QObject):
metadata["icon_database"] = database metadata["icon_database"] = database
return database return database
def update_component_ports(self, component: Component, ports: dict[PortID, Port]) -> None: def update_component_ports(self, component: Component, ports: dict[PortID, Port], port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
current = component.interface.ports current = component.interface.ports
removed = [ removed = [
RemovePortCommand(self, component, port_id) RemovePortCommand(self, component, port_id)
@@ -325,12 +373,23 @@ class Document(QObject):
if current[port_id] != ports[port_id] if current[port_id] != ports[port_id]
] ]
commands = [*removed, *added, *changed] commands = [*removed, *added, *changed]
if not commands: database = self.port_metadata_database()
component_port_ids = set(current) | set(ports)
for port_id in component_port_ids:
metadata = port_metadata.get(port_id, PortMetadata()) if port_metadata is not None and port_id in ports else PortMetadata()
if metadata == PortMetadata():
database.ports.pop(port_id, None)
else:
database.ports[port_id] = deepcopy(metadata)
metadata_changed = database != self.port_metadata_database()
if not commands and not metadata_changed:
return return
self.undo_stack.beginMacro("Edit interface") self.undo_stack.beginMacro("Edit interface")
for command in commands: for command in commands:
self.undo_stack.push(command) self.undo_stack.push(command)
if metadata_changed:
self.undo_stack.push(ChangePortMetadataDatabaseCommand(self, database))
self.undo_stack.endMacro() self.undo_stack.endMacro()
@@ -369,12 +428,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))
@@ -386,16 +455,16 @@ class Document(QObject):
self.undo_stack.push(DeleteComponent(self, component)) self.undo_stack.push(DeleteComponent(self, component))
self.undo_stack.endMacro() self.undo_stack.endMacro()
def paste_components(self, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None: def paste_components(self, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
if not components: if not components:
return return
names = {component.name for component in target.values()} names = {component.name for component in target.values()}
for component in components.values(): for component in components.values():
component.name = self._unique_name(names, component.name) component.name = self._unique_name(names, component.name)
names.add(component.name) names.add(component.name)
self.undo_stack.push(PasteComponents(self, target, components, icons)) self.undo_stack.push(PasteComponents(self, target, components, icons, port_metadata=port_metadata))
def paste_graph_components(self, graph_component: Component, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], positions: dict[ComponentID, tuple[int, int]]) -> None: def paste_graph_components(self, graph_component: Component, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], positions: dict[ComponentID, tuple[int, int]], port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
if not isinstance(graph_component.implementation, GraphImplementation) or not components: if not isinstance(graph_component.implementation, GraphImplementation) or not components:
return return
target = graph_component.implementation.graph.components target = graph_component.implementation.graph.components
@@ -403,7 +472,7 @@ class Document(QObject):
for component in components.values(): for component in components.values():
component.name = self._unique_name(names, component.name) component.name = self._unique_name(names, component.name)
names.add(component.name) names.add(component.name)
self.undo_stack.push(PasteComponents(self, target, components, icons, self.component_id(graph_component), positions)) self.undo_stack.push(PasteComponents(self, target, components, icons, self.component_id(graph_component), positions, port_metadata))
@staticmethod @staticmethod
def _unique_component_name(components: dict[ComponentID, Component], name: str) -> str: def _unique_component_name(components: dict[ComponentID, Component], name: str) -> str:

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")
@@ -123,6 +143,31 @@ class IconDatabase:
def to_data(self) -> dict[str, Any]: def to_data(self) -> dict[str, Any]:
return {"format_version": self.format_version, "icons": {str(key): icon.to_data() for key, icon in self.icons.items()}} return {"format_version": self.format_version, "icons": {str(key): icon.to_data() for key, icon in self.icons.items()}}
@dataclass
class PortMetadata:
connection_annotation: str | None = None
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> PortMetadata:
annotation = data.get("connection_annotation")
return cls(connection_annotation=str(annotation) if annotation else None)
def to_data(self) -> dict[str, Any]:
return {"connection_annotation": self.connection_annotation}
@dataclass
class PortMetadataDatabase:
format_version: int = 1
ports: dict[PortID, PortMetadata] = field(default_factory=dict)
@classmethod
def from_data(cls, data: Mapping[str, Any]) -> PortMetadataDatabase:
ports = {PortID(key): PortMetadata.from_data(value) for key, value in data.get("ports", {}).items()}
return cls(format_version=int(data.get("format_version", 1)), ports=ports)
def to_data(self) -> dict[str, Any]:
return {"format_version": self.format_version, "ports": {str(key): metadata.to_data() for key, metadata in self.ports.items()}}
@dataclass @dataclass
class GraphConnection: class GraphConnection:
points: list[tuple[int, int]] = field(default_factory=list) points: list[tuple[int, int]] = field(default_factory=list)

View File

@@ -6,7 +6,7 @@ from typing import Any
from bedit_core.models import Component, ComponentID, ConnectionID, Document, GraphImplementation, ID, ParameterID, PortID from bedit_core.models import Component, ComponentID, ConnectionID, Document, GraphImplementation, ID, ParameterID, PortID
from bedit_core.serialization.schema import document_from_data, document_to_data from bedit_core.serialization.schema import document_from_data, document_to_data
from bedit_gui.documents import Document as GuiDocument from bedit_gui.documents import Document as GuiDocument
from bedit_gui.models import Icon, ShapeID from bedit_gui.models import Icon, PortMetadata, ShapeID
FORMAT_VERSION = 1 FORMAT_VERSION = 1
COMPONENTS_MIME = "application/x-bedit-components+json" COMPONENTS_MIME = "application/x-bedit-components+json"
@@ -16,21 +16,27 @@ def export_components(document: GuiDocument, components: list[Component]) -> dic
roots = {document.component_id(component): component for component in components} roots = {document.component_id(component): component for component in components}
component_ids = _all_component_ids(roots) component_ids = _all_component_ids(roots)
icons = {} icons = {}
port_metadata = {}
for component_id in component_ids: for component_id in component_ids:
icon = document.stored_component_icon(component_id) icon = document.stored_component_icon(component_id)
if icon is not None: if icon is not None:
icons[component_id] = icon icons[component_id] = icon
return export_component_data(roots, icons) database = document.port_metadata_database()
for port_id in _all_port_ids(roots):
if port_id in database.ports:
port_metadata[port_id] = database.ports[port_id]
return export_component_data(roots, icons, port_metadata)
def export_component_data(components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> dict[str, Any]: def export_component_data(components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], port_metadata: dict[PortID, PortMetadata] | None = None) -> dict[str, Any]:
serialized = document_to_data(Document(format_version=1, id=ID(), name="Clipboard", root=components)) serialized = document_to_data(Document(format_version=1, id=ID(), name="Clipboard", root=components))
component_ids = set(_all_component_ids(components)) component_ids = set(_all_component_ids(components))
icon_data = {str(component_id): icon.to_data() for component_id, icon in icons.items() if component_id in component_ids} icon_data = {str(component_id): icon.to_data() for component_id, icon in icons.items() if component_id in component_ids}
return {"format_version": FORMAT_VERSION, "type": "components", "components": serialized["root"], "icons": icon_data} metadata_data = {str(port_id): metadata.to_data() for port_id, metadata in (port_metadata or {}).items() if port_id in set(_all_port_ids(components))}
return {"format_version": FORMAT_VERSION, "type": "components", "components": serialized["root"], "icons": icon_data, "port_metadata": metadata_data}
def import_components(payload: dict[str, Any]) -> tuple[dict[ComponentID, Component], dict[ComponentID, Icon]]: def import_components(payload: dict[str, Any]) -> tuple[dict[ComponentID, Component], dict[ComponentID, Icon], dict[PortID, PortMetadata]]:
if payload.get("format_version") != FORMAT_VERSION or payload.get("type") != "components": if payload.get("format_version") != FORMAT_VERSION or payload.get("type") != "components":
raise ValueError("unsupported component clipboard format") raise ValueError("unsupported component clipboard format")
components = payload.get("components") components = payload.get("components")
@@ -52,7 +58,11 @@ def import_components(payload: dict[str, Any]) -> tuple[dict[ComponentID, Compon
icon.shapes = {ShapeID(): shape for shape in icon.shapes.values()} icon.shapes = {ShapeID(): shape for shape in icon.shapes.values()}
icon.port_positions = {port_map[port_id]: position for port_id, position in icon.port_positions.items() if port_id in port_map} icon.port_positions = {port_map[port_id]: position for port_id, position in icon.port_positions.items() if port_id in port_map}
icons[new_component_id] = icon icons[new_component_id] = icon
return remapped, icons metadata_data = payload.get("port_metadata", {})
if not isinstance(metadata_data, dict):
raise TypeError("component clipboard port metadata must be an object")
port_metadata = {port_map[PortID(old_id)]: PortMetadata.from_data(data) for old_id, data in metadata_data.items() if PortID(old_id) in port_map and isinstance(data, dict)}
return remapped, icons, port_metadata
def _remap_components(components: dict[ComponentID, Component], component_map: dict[ComponentID, ComponentID], port_map: dict[PortID, PortID]) -> dict[ComponentID, Component]: def _remap_components(components: dict[ComponentID, Component], component_map: dict[ComponentID, ComponentID], port_map: dict[PortID, PortID]) -> dict[ComponentID, Component]:
@@ -87,3 +97,12 @@ def _all_component_ids(components: dict[ComponentID, Component]) -> list[Compone
if isinstance(component.implementation, GraphImplementation): if isinstance(component.implementation, GraphImplementation):
component_ids.extend(_all_component_ids(component.implementation.graph.components)) component_ids.extend(_all_component_ids(component.implementation.graph.components))
return component_ids return component_ids
def _all_port_ids(components: dict[ComponentID, Component]) -> list[PortID]:
port_ids: list[PortID] = []
for component in components.values():
port_ids.extend(component.interface.ports)
if isinstance(component.implementation, GraphImplementation):
port_ids.extend(_all_port_ids(component.implementation.graph.components))
return port_ids

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from bedit_core.models import Document from bedit_core.models import Document
from bedit_core.serialization import load as load_document from bedit_core.serialization import load as load_document
from bedit_core.serialization import save as save_document from bedit_core.serialization import save as save_document
from bedit_gui.models import GraphDatabase, IconDatabase, SimulationDatabase from bedit_gui.models import GraphDatabase, IconDatabase, PortMetadataDatabase, SimulationDatabase
def load(path: str | Path) -> Document: def load(path: str | Path) -> Document:
@@ -18,6 +18,8 @@ def load(path: str | Path) -> Document:
document.metadata["graph_database"] = GraphDatabase.from_data(document.metadata["graph_database"]) document.metadata["graph_database"] = GraphDatabase.from_data(document.metadata["graph_database"])
if document.metadata is not None and isinstance(document.metadata.get("simulation_database"), dict): if document.metadata is not None and isinstance(document.metadata.get("simulation_database"), dict):
document.metadata["simulation_database"] = SimulationDatabase.from_data(document.metadata["simulation_database"]) document.metadata["simulation_database"] = SimulationDatabase.from_data(document.metadata["simulation_database"])
if document.metadata is not None and isinstance(document.metadata.get("port_metadata_database"), dict):
document.metadata["port_metadata_database"] = PortMetadataDatabase.from_data(document.metadata["port_metadata_database"])
return document return document
@@ -30,4 +32,6 @@ def save(document: Document, path: str | Path) -> None:
saved_document.metadata["graph_database"] = saved_document.metadata["graph_database"].to_data() saved_document.metadata["graph_database"] = saved_document.metadata["graph_database"].to_data()
if saved_document.metadata is not None and isinstance(saved_document.metadata.get("simulation_database"), SimulationDatabase): if saved_document.metadata is not None and isinstance(saved_document.metadata.get("simulation_database"), SimulationDatabase):
saved_document.metadata["simulation_database"] = saved_document.metadata["simulation_database"].to_data() saved_document.metadata["simulation_database"] = saved_document.metadata["simulation_database"].to_data()
if saved_document.metadata is not None and isinstance(saved_document.metadata.get("port_metadata_database"), PortMetadataDatabase):
saved_document.metadata["port_metadata_database"] = saved_document.metadata["port_metadata_database"].to_data()
save_document(saved_document, path) save_document(saved_document, path)

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

@@ -148,6 +148,20 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0">
<widget class="QLabel" name="connectionAnnotationLabel">
<property name="text">
<string>Connection annotation:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="connectionAnnotationEdit">
<property name="placeholderText">
<string>For example, + or -</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item> <item>

View File

@@ -0,0 +1,268 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'port_editor_widget.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 (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QFormLayout,
QFrame, QHBoxLayout, QLabel, QLineEdit,
QListView, QPlainTextEdit, QPushButton, QRadioButton,
QSizePolicy, QSpacerItem, QSpinBox, QVBoxLayout,
QWidget)
class Ui_PortEditor(object):
def setupUi(self, PortEditor):
if not PortEditor.objectName():
PortEditor.setObjectName(u"PortEditor")
PortEditor.resize(541, 420)
self.horizontalLayout_2 = QHBoxLayout(PortEditor)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.leftColumn = QVBoxLayout()
self.leftColumn.setObjectName(u"leftColumn")
self.portList = QListView(PortEditor)
self.portList.setObjectName(u"portList")
self.leftColumn.addWidget(self.portList)
self.buttonRow = QHBoxLayout()
self.buttonRow.setObjectName(u"buttonRow")
self.addPort = QPushButton(PortEditor)
self.addPort.setObjectName(u"addPort")
self.buttonRow.addWidget(self.addPort)
self.removePort = QPushButton(PortEditor)
self.removePort.setObjectName(u"removePort")
self.buttonRow.addWidget(self.removePort)
self.leftColumn.addLayout(self.buttonRow)
self.horizontalLayout_2.addLayout(self.leftColumn)
self.rightColumn = QVBoxLayout()
self.rightColumn.setObjectName(u"rightColumn")
self.basicForm = QFormLayout()
self.basicForm.setObjectName(u"basicForm")
self.nameLabel = QLabel(PortEditor)
self.nameLabel.setObjectName(u"nameLabel")
self.basicForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel)
self.nameEdit = QLineEdit(PortEditor)
self.nameEdit.setObjectName(u"nameEdit")
self.basicForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
self.typeLabel = QLabel(PortEditor)
self.typeLabel.setObjectName(u"typeLabel")
self.basicForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.typeLabel)
self.typeRow = QHBoxLayout()
self.typeRow.setObjectName(u"typeRow")
self.typeSignal = QRadioButton(PortEditor)
self.typeSignal.setObjectName(u"typeSignal")
self.typeRow.addWidget(self.typeSignal)
self.typeBond = QRadioButton(PortEditor)
self.typeBond.setObjectName(u"typeBond")
self.typeRow.addWidget(self.typeBond)
self.basicForm.setLayout(1, QFormLayout.ItemRole.FieldRole, self.typeRow)
self.orientationLabel = QLabel(PortEditor)
self.orientationLabel.setObjectName(u"orientationLabel")
self.basicForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.orientationLabel)
self.orientationRow = QHBoxLayout()
self.orientationRow.setObjectName(u"orientationRow")
self.inputOrientation = QRadioButton(PortEditor)
self.inputOrientation.setObjectName(u"inputOrientation")
self.orientationRow.addWidget(self.inputOrientation)
self.outputOrientation = QRadioButton(PortEditor)
self.outputOrientation.setObjectName(u"outputOrientation")
self.orientationRow.addWidget(self.outputOrientation)
self.basicForm.setLayout(2, QFormLayout.ItemRole.FieldRole, self.orientationRow)
self.sizeLabel = QLabel(PortEditor)
self.sizeLabel.setObjectName(u"sizeLabel")
self.basicForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.sizeLabel)
self.sizeRow = QHBoxLayout()
self.sizeRow.setObjectName(u"sizeRow")
self.widthSize = QSpinBox(PortEditor)
self.widthSize.setObjectName(u"widthSize")
self.widthSize.setMinimum(1)
self.sizeRow.addWidget(self.widthSize)
self.heightSize = QSpinBox(PortEditor)
self.heightSize.setObjectName(u"heightSize")
self.heightSize.setMinimum(1)
self.sizeRow.addWidget(self.heightSize)
self.basicForm.setLayout(3, QFormLayout.ItemRole.FieldRole, self.sizeRow)
self.domainLabel = QLabel(PortEditor)
self.domainLabel.setObjectName(u"domainLabel")
self.basicForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.domainLabel)
self.multiplicityCheckBox = QCheckBox(PortEditor)
self.multiplicityCheckBox.setObjectName(u"multiplicityCheckBox")
self.basicForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.multiplicityCheckBox)
self.connectionAnnotationLabel = QLabel(PortEditor)
self.connectionAnnotationLabel.setObjectName(u"connectionAnnotationLabel")
self.basicForm.setWidget(5, QFormLayout.ItemRole.LabelRole, self.connectionAnnotationLabel)
self.connectionAnnotationEdit = QLineEdit(PortEditor)
self.connectionAnnotationEdit.setObjectName(u"connectionAnnotationEdit")
self.basicForm.setWidget(5, QFormLayout.ItemRole.FieldRole, self.connectionAnnotationEdit)
self.rightColumn.addLayout(self.basicForm)
self.line = QFrame(PortEditor)
self.line.setObjectName(u"line")
self.line.setFrameShape(QFrame.Shape.HLine)
self.line.setFrameShadow(QFrame.Shadow.Sunken)
self.rightColumn.addWidget(self.line)
self.signalOptions = QFormLayout()
self.signalOptions.setObjectName(u"signalOptions")
self.signalTypeLabel = QLabel(PortEditor)
self.signalTypeLabel.setObjectName(u"signalTypeLabel")
self.signalOptions.setWidget(0, QFormLayout.ItemRole.LabelRole, self.signalTypeLabel)
self.signalTypeComboBox = QComboBox(PortEditor)
self.signalTypeComboBox.setObjectName(u"signalTypeComboBox")
self.signalOptions.setWidget(0, QFormLayout.ItemRole.FieldRole, self.signalTypeComboBox)
self.rightColumn.addLayout(self.signalOptions)
self.bondOptions = QFormLayout()
self.bondOptions.setObjectName(u"bondOptions")
self.domainLabel_2 = QLabel(PortEditor)
self.domainLabel_2.setObjectName(u"domainLabel_2")
self.bondOptions.setWidget(0, QFormLayout.ItemRole.LabelRole, self.domainLabel_2)
self.domainComboBox = QComboBox(PortEditor)
self.domainComboBox.setObjectName(u"domainComboBox")
self.bondOptions.setWidget(0, QFormLayout.ItemRole.FieldRole, self.domainComboBox)
self.causalityLabel = QLabel(PortEditor)
self.causalityLabel.setObjectName(u"causalityLabel")
self.bondOptions.setWidget(1, QFormLayout.ItemRole.LabelRole, self.causalityLabel)
self.causalityComboBox = QComboBox(PortEditor)
self.causalityComboBox.setObjectName(u"causalityComboBox")
self.bondOptions.setWidget(1, QFormLayout.ItemRole.FieldRole, self.causalityComboBox)
self.rightColumn.addLayout(self.bondOptions)
self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.rightColumn.addItem(self.verticalSpacer)
self.line_2 = QFrame(PortEditor)
self.line_2.setObjectName(u"line_2")
self.line_2.setFrameShape(QFrame.Shape.HLine)
self.line_2.setFrameShadow(QFrame.Shadow.Sunken)
self.rightColumn.addWidget(self.line_2)
self.descriptionForm = QFormLayout()
self.descriptionForm.setObjectName(u"descriptionForm")
self.descriptionForm.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
self.descriptionLabel = QLabel(PortEditor)
self.descriptionLabel.setObjectName(u"descriptionLabel")
self.descriptionForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.descriptionLabel)
self.descriptionEdit = QPlainTextEdit(PortEditor)
self.descriptionEdit.setObjectName(u"descriptionEdit")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.MinimumExpanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.descriptionEdit.sizePolicy().hasHeightForWidth())
self.descriptionEdit.setSizePolicy(sizePolicy)
self.descriptionEdit.setMinimumSize(QSize(0, 20))
self.descriptionEdit.setMaximumSize(QSize(16777215, 60))
self.descriptionForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.descriptionEdit)
self.rightColumn.addLayout(self.descriptionForm)
self.horizontalLayout_2.addLayout(self.rightColumn)
self.retranslateUi(PortEditor)
QMetaObject.connectSlotsByName(PortEditor)
# setupUi
def retranslateUi(self, PortEditor):
PortEditor.setWindowTitle(QCoreApplication.translate("PortEditor", u"Form", None))
self.addPort.setText(QCoreApplication.translate("PortEditor", u"Add Port", None))
self.removePort.setText(QCoreApplication.translate("PortEditor", u"Remove Port", None))
self.nameLabel.setText(QCoreApplication.translate("PortEditor", u"Name:", None))
self.typeLabel.setText(QCoreApplication.translate("PortEditor", u"Type:", None))
self.typeSignal.setText(QCoreApplication.translate("PortEditor", u"Signal", None))
self.typeBond.setText(QCoreApplication.translate("PortEditor", u"Power Bond", None))
self.orientationLabel.setText(QCoreApplication.translate("PortEditor", u"Orientation:", None))
self.inputOrientation.setText(QCoreApplication.translate("PortEditor", u"Input", None))
self.outputOrientation.setText(QCoreApplication.translate("PortEditor", u"Output", None))
self.sizeLabel.setText(QCoreApplication.translate("PortEditor", u"Size", None))
self.widthSize.setSuffix(QCoreApplication.translate("PortEditor", u" rows", None))
self.heightSize.setSuffix(QCoreApplication.translate("PortEditor", u" columns", None))
self.domainLabel.setText("")
self.multiplicityCheckBox.setText(QCoreApplication.translate("PortEditor", u"Allow multiple connections", None))
self.connectionAnnotationLabel.setText(QCoreApplication.translate("PortEditor", u"Connection annotation:", None))
self.connectionAnnotationEdit.setPlaceholderText(QCoreApplication.translate("PortEditor", u"For example, + or -", None))
self.signalTypeLabel.setText(QCoreApplication.translate("PortEditor", u"Signal Type:", None))
self.domainLabel_2.setText(QCoreApplication.translate("PortEditor", u"Domain:", None))
self.causalityLabel.setText(QCoreApplication.translate("PortEditor", u"Causality:", None))
self.descriptionLabel.setText(QCoreApplication.translate("PortEditor", u"Description:", 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

@@ -8,6 +8,7 @@ from PySide6.QtWidgets import (
) )
from bedit_core.models import Port, PortID from bedit_core.models import Port, PortID
from bedit_gui.models import PortMetadata
from bedit_gui.views.port_editor_widget import PortEditorWidget from bedit_gui.views.port_editor_widget import PortEditorWidget
@@ -17,6 +18,7 @@ class InterfaceEditorDialog(QDialog):
def __init__( def __init__(
self, self,
ports: dict[PortID, Port], ports: dict[PortID, Port],
port_metadata: dict[PortID, PortMetadata] | None = None,
parent: QWidget | None = None, parent: QWidget | None = None,
) -> None: ) -> None:
super().__init__(parent) super().__init__(parent)
@@ -25,7 +27,7 @@ class InterfaceEditorDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
self.editor = PortEditorWidget(self) self.editor = PortEditorWidget(self)
self.editor.set_ports(ports) self.editor.set_ports(ports, port_metadata)
layout.addWidget(self.editor) layout.addWidget(self.editor)
buttons = QDialogButtonBox( buttons = QDialogButtonBox(
@@ -38,3 +40,6 @@ class InterfaceEditorDialog(QDialog):
def ports(self) -> dict[PortID, Port]: def ports(self) -> dict[PortID, Port]:
return self.editor.ports() return self.editor.ports()
def port_metadata(self) -> dict[PortID, PortMetadata]:
return self.editor.port_metadata()

View File

@@ -3,7 +3,8 @@ from __future__ import annotations
from PySide6.QtCore import QEvent, QObject, QTimer, Qt, Signal from PySide6.QtCore import QEvent, QObject, QTimer, Qt, Signal
from PySide6.QtWidgets import QWidget from PySide6.QtWidgets import QWidget
from bedit_core.models import Component, EquationImplementation from bedit_core.models import Component, EquationImplementation, PortID
from bedit_gui.models import PortMetadata
from bedit_gui.ui.generated.ui_equation_editor_widget import Ui_equationEditorWidget from bedit_gui.ui.generated.ui_equation_editor_widget import Ui_equationEditorWidget
from bedit_gui.views.param_editor_widget import ParamEditorWidget from bedit_gui.views.param_editor_widget import ParamEditorWidget
from bedit_gui.views.port_editor_widget import PortEditorWidget from bedit_gui.views.port_editor_widget import PortEditorWidget
@@ -14,6 +15,7 @@ class EquationEditorWidget(QWidget):
component_changed = Signal(object) component_changed = Signal(object)
equation_text_change_requested = Signal(object, str, object, int) equation_text_change_requested = Signal(object, str, object, int)
port_metadata_change_requested = Signal(object, object)
sidebar_visible_changed = Signal(bool) sidebar_visible_changed = Signal(bool)
def __init__(self, parent: QWidget | None = None) -> None: def __init__(self, parent: QWidget | None = None) -> None:
@@ -22,6 +24,7 @@ class EquationEditorWidget(QWidget):
self.ui = Ui_equationEditorWidget() self.ui = Ui_equationEditorWidget()
self.ui.setupUi(self) self.ui.setupUi(self)
self._component: Component | None = None self._component: Component | None = None
self._port_metadata: dict[PortID, PortMetadata] = {}
self._loading = False self._loading = False
self._edit_id = 0 self._edit_id = 0
self._edit_timer = QTimer(self) self._edit_timer = QTimer(self)
@@ -45,11 +48,12 @@ class EquationEditorWidget(QWidget):
self._set_editors_enabled(False) self._set_editors_enabled(False)
def set_component(self, component: Component | None) -> None: def set_component(self, component: Component | None, port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
if component is not None and not isinstance(component.implementation, EquationImplementation): if component is not None and not isinstance(component.implementation, EquationImplementation):
raise TypeError("EquationEditorWidget only supports components with an equation implementation") raise TypeError("EquationEditorWidget only supports components with an equation implementation")
self._component = component self._component = component
self._port_metadata = port_metadata or {}
self.refresh() self.refresh()
def component(self) -> Component | None: def component(self) -> Component | None:
@@ -88,7 +92,7 @@ class EquationEditorWidget(QWidget):
self.ui.initialEquationsTextEdit.setPlainText("\n".join(implementation.initial_equations)) self.ui.initialEquationsTextEdit.setPlainText("\n".join(implementation.initial_equations))
self.ui.equationsTextEdit.setPlainText("\n".join(implementation.equations)) self.ui.equationsTextEdit.setPlainText("\n".join(implementation.equations))
self.param_editor.set_params(component.parameters) self.param_editor.set_params(component.parameters)
self.port_editor.set_ports(component.interface.ports) self.port_editor.set_ports(component.interface.ports, self._port_metadata)
self._loading = False self._loading = False
self._set_editors_enabled(component is not None) self._set_editors_enabled(component is not None)
@@ -145,8 +149,21 @@ class EquationEditorWidget(QWidget):
def _ports_changed(self) -> None: def _ports_changed(self) -> None:
if self._loading or self._component is None: if self._loading or self._component is None:
return return
self._component.interface.ports = self.port_editor.ports() ports = self.port_editor.ports()
self.component_changed.emit(self._component) metadata = self.port_editor.port_metadata()
if ports != self._component.interface.ports:
self._component.interface.ports = ports
self.component_changed.emit(self._component)
if metadata != self._port_metadata:
self._port_metadata = metadata
self.port_metadata_change_requested.emit(self._component, metadata)
def refresh_port_metadata(self, port_metadata: dict[PortID, PortMetadata]) -> None:
if port_metadata == self._port_metadata:
return
self._port_metadata = port_metadata
if self._component is not None:
self.port_editor.set_ports(self._component.interface.ports, port_metadata)
def _replace_placeholder(self, placeholder: QWidget, editor: QWidget) -> None: def _replace_placeholder(self, placeholder: QWidget, editor: QWidget) -> None:
self.ui.verticalLayout_2.replaceWidget(placeholder, editor) self.ui.verticalLayout_2.replaceWidget(placeholder, editor)

View File

@@ -10,7 +10,7 @@ from PySide6.QtGui import QActionGroup, QBrush, QColor, QCursor, QDragEnterEvent
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
from bedit_core.models import BondCausality, BondConnection, BondPort, Component, ComponentID, Connection, ConnectionID, GraphImplementation, Port, PortID, SignalConnection, SignalDirection, SignalPort from bedit_core.models import BondCausality, BondConnection, BondPort, Component, ComponentID, Connection, ConnectionID, GraphImplementation, Port, PortID, SignalConnection, SignalDirection, SignalPort
from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, Icon from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, Icon, PortMetadata
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_natural_icon_size, get_pixmap_bounding_box, render_icon from bedit_gui.utils.icon import get_natural_icon_size, get_pixmap_bounding_box, render_icon
@@ -34,6 +34,9 @@ ARROW_HALF_WIDTH = 16.0
SIGNAL_ARROW_LENGTH = ARROW_LENGTH / 2 SIGNAL_ARROW_LENGTH = ARROW_LENGTH / 2
SIGNAL_ARROW_HALF_WIDTH = ARROW_HALF_WIDTH / 2 SIGNAL_ARROW_HALF_WIDTH = ARROW_HALF_WIDTH / 2
CAUSALITY_TICK_HALF_LENGTH = 16.0 CAUSALITY_TICK_HALF_LENGTH = 16.0
CONNECTION_ANNOTATION_FONT_SIZE = 18.0
CONNECTION_ANNOTATION_BACK_OFFSET = 24.0
CONNECTION_ANNOTATION_SIDE_OFFSET = 14.0
class GraphEditorMode(Enum): class GraphEditorMode(Enum):
@@ -152,7 +155,7 @@ class GraphComponentItem(QGraphicsPixmapItem):
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges) self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object: def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object:
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._dragging and isinstance(value, QPointF): if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self.component_id in self.editor._component_drag_starts and isinstance(value, QPointF):
grid_size = self.editor.snap_to_grid_size grid_size = self.editor.snap_to_grid_size
value = QPointF(round(value.x() / grid_size) * grid_size, round(value.y() / grid_size) * grid_size) value = QPointF(round(value.x() / grid_size) * grid_size, round(value.y() / grid_size) * grid_size)
result = super().itemChange(change, value) result = super().itemChange(change, value)
@@ -162,23 +165,22 @@ 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())
self._dragging = True self._dragging = True
super().mousePressEvent(event) super().mousePressEvent(event)
self.editor.begin_component_move()
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)
self._dragging = False self._dragging = False
position = (round(self.pos().x()), round(self.pos().y())) self.editor.finish_component_moves()
self.setPos(*position)
if self.pos() != self._drag_start:
self.editor.finish_component_move(self.component_id, position)
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None: def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if event.button() == Qt.MouseButton.LeftButton: if event.button() == Qt.MouseButton.LeftButton:
@@ -294,7 +296,7 @@ class GraphConnectionPointItem(QGraphicsEllipseItem):
class GraphEditorWidget(QWidget): class GraphEditorWidget(QWidget):
component_move_requested = Signal(object, object, object) component_moves_requested = Signal(object, object)
component_context_menu_requested = Signal(object, object) component_context_menu_requested = Signal(object, object)
component_open_requested = Signal(object) component_open_requested = Signal(object)
component_label_move_requested = Signal(object, object, object) component_label_move_requested = Signal(object, object, object)
@@ -311,14 +313,17 @@ class GraphEditorWidget(QWidget):
self._component: Component | None = None self._component: Component | None = None
self._graph = Graph() self._graph = Graph()
self._icons: dict[ComponentID, Icon] = {} self._icons: dict[ComponentID, Icon] = {}
self._port_metadata: dict[PortID, PortMetadata] = {}
self._mode = GraphEditorMode.NORMAL self._mode = GraphEditorMode.NORMAL
self._connection_start: ComponentID | None = None self._connection_start: ComponentID | None = None
self._connection_preview: QGraphicsPathItem | None = None self._connection_preview: QGraphicsPathItem | None = None
self._component_items: dict[ComponentID, GraphComponentItem] = {} self._component_items: dict[ComponentID, GraphComponentItem] = {}
self._component_drag_starts: dict[ComponentID, tuple[int, int]] = {}
self._component_bounds: dict[ComponentID, QRectF] = {} self._component_bounds: dict[ComponentID, QRectF] = {}
self._component_label_items: dict[ComponentID, GraphComponentLabelItem] = {} self._component_label_items: dict[ComponentID, GraphComponentLabelItem] = {}
self._connection_items: dict[ConnectionID, GraphConnectionItem] = {} self._connection_items: dict[ConnectionID, GraphConnectionItem] = {}
self._connection_point_items: dict[ConnectionID, list[GraphConnectionPointItem]] = {} self._connection_point_items: dict[ConnectionID, list[GraphConnectionPointItem]] = {}
self._connection_annotation_items: dict[ConnectionID, QGraphicsTextItem] = {}
self.set_snap_to_grid_size(snap_to_grid_size) self.set_snap_to_grid_size(snap_to_grid_size)
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)
@@ -363,12 +368,12 @@ class GraphEditorWidget(QWidget):
self._clear_connection_start() self._clear_connection_start()
component = self._component component = self._component
if component is not None: if component is not None:
self.set_component(component, self._graph, self._icons) self.set_component(component, self._graph, self._icons, self._port_metadata)
def toggle_mode(self) -> None: def toggle_mode(self) -> None:
self.set_mode(GraphEditorMode.CONNECTION if self._mode is GraphEditorMode.NORMAL else GraphEditorMode.NORMAL) self.set_mode(GraphEditorMode.CONNECTION if self._mode is GraphEditorMode.NORMAL else GraphEditorMode.NORMAL)
def set_component(self, component: Component | None, graph: Graph | None = None, icons: dict[ComponentID, Icon] | None = None) -> None: def set_component(self, component: Component | None, graph: Graph | None = None, icons: dict[ComponentID, Icon] | None = None, port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
if component is not None and not isinstance(component.implementation, GraphImplementation): if component is not None and not isinstance(component.implementation, GraphImplementation):
raise TypeError("GraphEditorWidget only supports components with a graph implementation") raise TypeError("GraphEditorWidget only supports components with a graph implementation")
component_changed = component is not self._component component_changed = component is not self._component
@@ -376,11 +381,14 @@ class GraphEditorWidget(QWidget):
self._component = component self._component = component
self._graph = graph or Graph() self._graph = graph or Graph()
self._icons = icons or {} self._icons = icons or {}
self._port_metadata = port_metadata or {}
self._component_drag_starts = {}
self._component_items = {} self._component_items = {}
self._component_bounds = {} self._component_bounds = {}
self._component_label_items = {} self._component_label_items = {}
self._connection_items = {} self._connection_items = {}
self._connection_point_items = {} self._connection_point_items = {}
self._connection_annotation_items = {}
self.scene.clear() self.scene.clear()
if component is None: if component is None:
return return
@@ -419,6 +427,17 @@ class GraphEditorWidget(QWidget):
connection_item.setData(0, str(connection_id)) connection_item.setData(0, str(connection_id))
self._connection_items[connection_id] = connection_item self._connection_items[connection_id] = connection_item
self.scene.addItem(connection_item) self.scene.addItem(connection_item)
annotation = self._port_metadata.get(connection.target, PortMetadata()).connection_annotation if isinstance(connection, SignalConnection) else None
if annotation:
annotation_item = QGraphicsTextItem(annotation)
font = annotation_item.font()
font.setBold(True)
font.setPointSizeF(CONNECTION_ANNOTATION_FONT_SIZE)
annotation_item.setFont(font)
annotation_item.setDefaultTextColor(QColor(SIGNAL_CONNECTION_COLOR))
annotation_item.setZValue(1)
self._connection_annotation_items[connection_id] = annotation_item
self.scene.addItem(annotation_item)
visual_connection = graph.connections.get(connection_id) visual_connection = graph.connections.get(connection_id)
self._create_connection_point_items(connection_id, visual_connection.points[1:-1] if visual_connection is not None and len(visual_connection.points) >= 2 else []) self._create_connection_point_items(connection_id, visual_connection.points[1:-1] if visual_connection is not None and len(visual_connection.points) >= 2 else [])
self.refresh_connections() self.refresh_connections()
@@ -451,6 +470,23 @@ class GraphEditorWidget(QWidget):
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
item.setPath(item._connection_path(points, isinstance(connection, BondConnection), tick_at_source)) item.setPath(item._connection_path(points, isinstance(connection, BondConnection), tick_at_source))
annotation_item = self._connection_annotation_items.get(connection_id)
if annotation_item is not None:
self._position_connection_annotation(annotation_item, points)
@staticmethod
def _position_connection_annotation(item: QGraphicsTextItem, points: list[tuple[float, float]]) -> None:
target = QPointF(*points[-1])
previous = next((QPointF(*point) for point in reversed(points[:-1]) if QPointF(*point) != target), None)
if previous is None:
return
dx = target.x() - previous.x()
dy = target.y() - previous.y()
length = hypot(dx, dy)
x = target.x() - CONNECTION_ANNOTATION_BACK_OFFSET * dx / length - CONNECTION_ANNOTATION_SIDE_OFFSET * dy / length
y = target.y() - CONNECTION_ANNOTATION_BACK_OFFSET * dy / length + CONNECTION_ANNOTATION_SIDE_OFFSET * dx / length
bounds = item.boundingRect()
item.setPos(x - bounds.width() / 2, y - bounds.height() / 2)
def add_connection_point(self, connection_id: ConnectionID, scene_position: QPointF) -> None: def add_connection_point(self, connection_id: ConnectionID, scene_position: QPointF) -> None:
points = self._connection_metadata_points(connection_id) points = self._connection_metadata_points(connection_id)
@@ -476,6 +512,9 @@ class GraphEditorWidget(QWidget):
connection_item = self._connection_items.pop(connection_id, None) connection_item = self._connection_items.pop(connection_id, None)
if connection_item is not None: if connection_item is not None:
self.scene.removeItem(connection_item) self.scene.removeItem(connection_item)
annotation_item = self._connection_annotation_items.pop(connection_id, None)
if annotation_item is not None:
self.scene.removeItem(annotation_item)
return return
self._graph.connections[connection_id] = GraphConnection(points=list(points)) self._graph.connections[connection_id] = GraphConnection(points=list(points))
interior_points = points[1:-1] interior_points = points[1:-1]
@@ -531,9 +570,23 @@ class GraphEditorWidget(QWidget):
best_distance = distance best_distance = distance
return best_index return best_index
def finish_component_move(self, component_id: ComponentID, position: tuple[int, int]) -> None: def begin_component_move(self) -> None:
if self._component is not None: self._component_drag_starts = {item.component_id: (round(item.pos().x()), round(item.pos().y())) for item in self.scene.selectedItems() if isinstance(item, GraphComponentItem)}
self.component_move_requested.emit(self._component, component_id, position)
def finish_component_moves(self) -> None:
starts = self._component_drag_starts
self._component_drag_starts = {}
if self._component is None:
return
positions = {}
for component_id, old_position in starts.items():
item = self._component_items[component_id]
position = (round(item.pos().x()), round(item.pos().y()))
item.setPos(*position)
if position != old_position:
positions[component_id] = position
if positions:
self.component_moves_requested.emit(self._component, positions)
def finish_component_label_move(self, component_id: ComponentID, relative_position: tuple[int, int]) -> None: def finish_component_label_move(self, component_id: ComponentID, relative_position: tuple[int, int]) -> None:
if self._component is not None: if self._component is not None:
@@ -723,7 +776,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

@@ -7,6 +7,7 @@ from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import QButtonGroup, QLayout, QWidget from PySide6.QtWidgets import QButtonGroup, QLayout, QWidget
from bedit_core.models import BondPort, Port, PortCausality, PortID, SignalDirection, SignalPort, ValueType from bedit_core.models import BondPort, Port, PortCausality, PortID, SignalDirection, SignalPort, ValueType
from bedit_gui.models import PortMetadata
from bedit_gui.ui.generated.ui_port_editor_widget import Ui_PortEditor from bedit_gui.ui.generated.ui_port_editor_widget import Ui_PortEditor
@@ -21,6 +22,7 @@ class PortEditorWidget(QWidget):
self.ui = Ui_PortEditor() self.ui = Ui_PortEditor()
self.ui.setupUi(self) self.ui.setupUi(self)
self._ports: dict[PortID, Port] = {} self._ports: dict[PortID, Port] = {}
self._port_metadata: dict[PortID, PortMetadata] = {}
self._port_ids: list[PortID] = [] self._port_ids: list[PortID] = []
self._loading = False self._loading = False
@@ -54,6 +56,7 @@ class PortEditorWidget(QWidget):
self.ui.widthSize.valueChanged.connect(self._form_changed) self.ui.widthSize.valueChanged.connect(self._form_changed)
self.ui.heightSize.valueChanged.connect(self._form_changed) self.ui.heightSize.valueChanged.connect(self._form_changed)
self.ui.multiplicityCheckBox.toggled.connect(self._form_changed) self.ui.multiplicityCheckBox.toggled.connect(self._form_changed)
self.ui.connectionAnnotationEdit.textChanged.connect(self._form_changed)
self.ui.signalTypeComboBox.currentIndexChanged.connect(self._form_changed) self.ui.signalTypeComboBox.currentIndexChanged.connect(self._form_changed)
self.ui.domainComboBox.currentTextChanged.connect(self._form_changed) self.ui.domainComboBox.currentTextChanged.connect(self._form_changed)
self.ui.causalityComboBox.currentIndexChanged.connect(self._form_changed) self.ui.causalityComboBox.currentIndexChanged.connect(self._form_changed)
@@ -62,14 +65,18 @@ class PortEditorWidget(QWidget):
self._set_editor_enabled(False) self._set_editor_enabled(False)
self._update_option_visibility() self._update_option_visibility()
def set_ports(self, ports: dict[PortID, Port]) -> None: def set_ports(self, ports: dict[PortID, Port], port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
self._ports = deepcopy(ports) self._ports = deepcopy(ports)
self._port_metadata = deepcopy(port_metadata or {})
self._port_ids = list(self._ports) self._port_ids = list(self._ports)
self._rebuild_list() self._rebuild_list()
def ports(self) -> dict[PortID, Port]: def ports(self) -> dict[PortID, Port]:
return deepcopy(self._ports) return deepcopy(self._ports)
def port_metadata(self) -> dict[PortID, PortMetadata]:
return deepcopy(self._port_metadata)
def _rebuild_list(self, selected_id: PortID | None = None) -> None: def _rebuild_list(self, selected_id: PortID | None = None) -> None:
self._list_model.clear() self._list_model.clear()
for port_id in self._port_ids: for port_id in self._port_ids:
@@ -90,7 +97,7 @@ class PortEditorWidget(QWidget):
self.ui.removePort.setEnabled(port_id is not None) self.ui.removePort.setEnabled(port_id is not None)
self._set_editor_enabled(port_id is not None) self._set_editor_enabled(port_id is not None)
if port_id is not None: if port_id is not None:
self._load_port(self._ports[port_id]) self._load_port(self._ports[port_id], self._port_metadata.get(port_id, PortMetadata()))
def _selected_port_id(self) -> PortID | None: def _selected_port_id(self) -> PortID | None:
index = self.ui.portList.currentIndex() index = self.ui.portList.currentIndex()
@@ -98,7 +105,7 @@ class PortEditorWidget(QWidget):
return None return None
return self._port_ids[index.row()] return self._port_ids[index.row()]
def _load_port(self, port: Port) -> None: def _load_port(self, port: Port, metadata: PortMetadata) -> None:
self._loading = True self._loading = True
self.ui.nameEdit.setText(port.name) self.ui.nameEdit.setText(port.name)
self.ui.inputOrientation.setChecked(port.direction is SignalDirection.INPUT) self.ui.inputOrientation.setChecked(port.direction is SignalDirection.INPUT)
@@ -106,6 +113,7 @@ class PortEditorWidget(QWidget):
self.ui.widthSize.setValue(port.matrix_size[0]) self.ui.widthSize.setValue(port.matrix_size[0])
self.ui.heightSize.setValue(port.matrix_size[1]) self.ui.heightSize.setValue(port.matrix_size[1])
self.ui.multiplicityCheckBox.setChecked(port.multiplicity) self.ui.multiplicityCheckBox.setChecked(port.multiplicity)
self.ui.connectionAnnotationEdit.setText(metadata.connection_annotation or "")
self.ui.descriptionEdit.setPlainText(port.description or "") self.ui.descriptionEdit.setPlainText(port.description or "")
if isinstance(port, SignalPort): if isinstance(port, SignalPort):
@@ -135,6 +143,12 @@ class PortEditorWidget(QWidget):
old_port = self._ports[port_id] old_port = self._ports[port_id]
self._ports[port_id] = self._port_from_form(old_port) self._ports[port_id] = self._port_from_form(old_port)
annotation = (self.ui.connectionAnnotationEdit.text().strip() or None) if self.ui.multiplicityCheckBox.isChecked() else None
metadata = PortMetadata(connection_annotation=annotation)
if metadata == PortMetadata():
self._port_metadata.pop(port_id, None)
else:
self._port_metadata[port_id] = metadata
self._list_model.item(self._port_ids.index(port_id)).setText( self._list_model.item(self._port_ids.index(port_id)).setText(
self._ports[port_id].name self._ports[port_id].name
) )
@@ -188,6 +202,7 @@ class PortEditorWidget(QWidget):
return return
row = self._port_ids.index(port_id) row = self._port_ids.index(port_id)
del self._ports[port_id] del self._ports[port_id]
self._port_metadata.pop(port_id, None)
self._port_ids.remove(port_id) self._port_ids.remove(port_id)
selected = ( selected = (
self._port_ids[min(row, len(self._port_ids) - 1)] self._port_ids[min(row, len(self._port_ids) - 1)]
@@ -201,6 +216,9 @@ class PortEditorWidget(QWidget):
signal = self.ui.typeSignal.isChecked() signal = self.ui.typeSignal.isChecked()
self._set_layout_visible(self.ui.signalOptions, signal) self._set_layout_visible(self.ui.signalOptions, signal)
self._set_layout_visible(self.ui.bondOptions, not signal) self._set_layout_visible(self.ui.bondOptions, not signal)
annotation_visible = self.ui.multiplicityCheckBox.isChecked()
self.ui.connectionAnnotationLabel.setVisible(annotation_visible)
self.ui.connectionAnnotationEdit.setVisible(annotation_visible)
@staticmethod @staticmethod
def _set_layout_visible(layout: QLayout, visible: bool) -> None: def _set_layout_visible(layout: QLayout, visible: bool) -> None:

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,387 @@
"end for;" "end for;"
] ]
} }
},
"404e82f4-19e2-44ad-b409-3f021e483a39": {
"name": "integrator",
"interface": {
"ports": {
"441a44d0-bbe5-49f0-bb0e-5bc891218aca": {
"port_type": "signal",
"name": "y",
"direction": "output",
"multiplicity": false,
"matrix_size": [
1,
1
],
"description": null,
"value_type": "real",
"quantity": null,
"unit": null
},
"8d4b0f2c-2e13-4cfd-a8da-6c03ade4fbbf": {
"port_type": "signal",
"name": "u",
"direction": "input",
"multiplicity": false,
"matrix_size": [
1,
1
],
"description": null,
"value_type": "real",
"quantity": null,
"unit": null
}
}
},
"parameters": {
"7150c82e-8c78-4f83-9bc8-e46057a367a8": {
"name": "k",
"value": "1.0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": "Integrator gain"
},
"f7b9691a-22a4-41e0-9871-44f8dd0e02d2": {
"name": "initType",
"value": "3",
"value_type": "int",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": "Type of initialization (1: no init, 2: steady state, 3: initial state, 4: initial output)"
},
"3b636a9c-257c-4368-855a-094051f29041": {
"name": "y_start",
"value": "0.0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": "Initial or guess value of output (= state)"
}
},
"implementation": {
"implementation_type": "equation",
"declarations": [],
"initial_equations": [
"if initType == 2 then",
"\tder(y) = 0;",
"elseif initType >= 3 then",
"\ty = y_start;",
"end if;"
],
"equations": [
"der(y) = k*u;"
]
}
},
"2b0156f6-9977-4735-88dd-14f5eb3d87b5": {
"name": "derivative",
"interface": {
"ports": {
"d61847c9-0628-4e86-ad91-9390f92f926d": {
"port_type": "signal",
"name": "y",
"direction": "output",
"multiplicity": false,
"matrix_size": [
1,
1
],
"description": null,
"value_type": "real",
"quantity": null,
"unit": null
},
"f18c8fd2-dda5-4e8d-a5a7-a22a38abb091": {
"port_type": "signal",
"name": "u",
"direction": "input",
"multiplicity": false,
"matrix_size": [
1,
1
],
"description": null,
"value_type": "real",
"quantity": null,
"unit": null
}
}
},
"parameters": {
"ec421126-1ff4-4627-8bdd-1841318f3eb6": {
"name": "k",
"value": "1.0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": "Integrator gain"
},
"0c364769-5f07-4f3e-bb93-91d86a1f2dae": {
"name": "initType",
"value": "3",
"value_type": "int",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": "Type of initialization (1: no init, 2: steady state, 3: initial state, 4: initial output)"
},
"ab3a2bae-ea7f-41ca-9493-93d8debcee27": {
"name": "y_start",
"value": "0.0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": "Initial or guess value of output (= state)"
},
"e04d9fc5-761c-4b3d-bd7b-93da9896bd35": {
"name": "T",
"value": "0.01",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": "Time constants (T>0 required; T=0 is ideal derivative block)"
},
"29669830-2738-4692-8255-115d1546619c": {
"name": "x_start",
"value": "0.0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": "Initial or guess value of state\""
}
},
"implementation": {
"implementation_type": "equation",
"declarations": [
"parameter Boolean zeroGain = abs(k) < Modelica.Constants.eps;",
"Real x(start=x_start);"
],
"initial_equations": [
"if initType == 2 then",
"\tder(x) = 0;",
"elseif initType == 3 then",
"\tx = x_start;",
"elseif initType >= 4 then",
"\tif zeroGain then",
"\t\tx = u;",
"\telse",
"\t\ty = y_start;",
"\tend if;",
"end if;"
],
"equations": [
"der(x) = if zeroGain then 0 else (u-x)/T;",
"y = if zeroGain then 0 else (k/T)*(u-x);"
]
}
},
"927326ff-f73a-4f98-b7fd-aaf97989121e": {
"name": "pulse",
"interface": {
"ports": {
"4f9f6b29-36f0-460e-9fd9-d4bdf0ea3c80": {
"port_type": "signal",
"name": "y",
"direction": "output",
"multiplicity": false,
"matrix_size": [
1,
1
],
"description": null,
"value_type": "real",
"quantity": null,
"unit": null
}
}
},
"parameters": {
"b6460a26-0b89-49e0-8ec7-99281a66668b": {
"name": "amplitude",
"value": "1.0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": null
},
"983d6c65-8e81-4f49-9faa-d024e8e6a9d3": {
"name": "width",
"value": "50.0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": "Pulse width in %"
},
"c9ccd873-f83a-4b08-8827-4a88a06d502a": {
"name": "offset",
"value": "0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": null
},
"9cd46f38-6ddb-4d69-946d-df96e9f30078": {
"name": "period",
"value": "1.0",
"value_type": "real",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": null
},
"4e1d29eb-48c8-479a-8968-8f57fd95ea9f": {
"name": "nperiod",
"value": "-1",
"value_type": "int",
"matrix_size": [
1,
1
],
"quantity": null,
"unit": null,
"description": "Number of periods (<0 means infinite)"
},
"68102f0d-b58b-4c10-af0d-dffae406fb07": {
"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);"
]
}
},
"ac215607-dd97-4142-b42f-f23502e51cfa": {
"name": "plusminus",
"interface": {
"ports": {
"baadcb27-0807-4366-819f-7396a1ed205d": {
"port_type": "signal",
"name": "plus",
"direction": "input",
"multiplicity": true,
"matrix_size": [
1,
1
],
"description": null,
"value_type": "real",
"quantity": null,
"unit": null
},
"53cc98de-2e05-49ea-af26-e26d5dcd99e7": {
"port_type": "signal",
"name": "minus",
"direction": "input",
"multiplicity": true,
"matrix_size": [
1,
1
],
"description": null,
"value_type": "real",
"quantity": null,
"unit": null
},
"0574652d-df52-4798-8072-da0a26eee873": {
"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 +1042,26 @@
"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"
},
"122eaf51-7baf-457f-a944-f946b6ab321a": {
"connection_type": "signal",
"source": "4f9f6b29-36f0-460e-9fd9-d4bdf0ea3c80",
"target": "baadcb27-0807-4366-819f-7396a1ed205d"
},
"8b1c34a9-03c2-4bb5-81ab-7fa66bcafe8e": {
"connection_type": "signal",
"source": "5807dc83-d049-4f0f-9708-d041a752fcd0",
"target": "53cc98de-2e05-49ea-af26-e26d5dcd99e7"
},
"3c659d9e-8218-48ac-9a3d-4297fba275c2": {
"connection_type": "signal",
"source": "0574652d-df52-4798-8072-da0a26eee873",
"target": "8d4b0f2c-2e13-4cfd-a8da-6c03ade4fbbf"
},
"9b5ab2d7-d302-48e8-98dc-0d60ba54ca01": {
"connection_type": "signal",
"source": "0574652d-df52-4798-8072-da0a26eee873",
"target": "f18c8fd2-dda5-4e8d-a5a7-a22a38abb091"
} }
} }
} }
@@ -1149,6 +1550,286 @@
-32 -32
] ]
} }
},
"404e82f4-19e2-44ad-b409-3f021e483a39": {
"shapes": {
"29dafe3d-8392-49c0-98ee-80e941b32a52": {
"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"
},
"d24da1c9-bae9-41e6-aee4-79e5f238ea72": {
"layer": 1,
"type": "text",
"pos": [
-15,
-47
],
"width": 31.0,
"height": 81.0,
"color": "#00007fff",
"bold": false,
"italic": false,
"size": 72.0,
"text": "∫"
}
},
"port_positions": {
"441a44d0-bbe5-49f0-bb0e-5bc891218aca": [
7,
-7
],
"8d4b0f2c-2e13-4cfd-a8da-6c03ade4fbbf": [
-22,
-7
]
}
},
"2b0156f6-9977-4735-88dd-14f5eb3d87b5": {
"shapes": {
"702ce5c1-a37a-4f89-8261-0be4b433ae79": {
"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"
},
"32ccfd50-07dd-4524-be5d-e96a3136d520": {
"layer": 1,
"type": "text",
"pos": [
-40,
-31
],
"width": 78.0,
"height": 62.0,
"color": "#00007fff",
"bold": false,
"italic": false,
"size": 38.0,
"text": "d/dt"
}
},
"port_positions": {
"d61847c9-0628-4e86-ad91-9390f92f926d": [
7,
-7
],
"f18c8fd2-dda5-4e8d-a5a7-a22a38abb091": [
-22,
-7
]
}
},
"927326ff-f73a-4f98-b7fd-aaf97989121e": {
"shapes": {
"89e11690-3faa-482b-a054-10a2c7565c43": {
"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"
},
"de170a5b-c959-4d6d-afda-fa867dfbca51": {
"layer": 4,
"type": "line",
"pos": [
-32,
-32
],
"end": [
-32,
32
],
"line_type": "solid",
"line_thickness": 3.0,
"line_color": "#000000ff"
},
"94fc1429-0ef2-403b-aa5d-7edcb7b3ebf2": {
"layer": 2,
"type": "line",
"pos": [
-32,
32
],
"end": [
32,
32
],
"line_type": "solid",
"line_thickness": 3.0,
"line_color": "#000000ff"
},
"0d82b577-e3fb-4434-adfb-883e1918e30f": {
"layer": 3,
"type": "line",
"pos": [
-32,
14
],
"end": [
-14,
14
],
"line_type": "solid",
"line_thickness": 2.0,
"line_color": "#ffaa00ff"
},
"531dcbea-2c2d-4ed9-bcbc-dfc9b0145527": {
"layer": 5,
"type": "line",
"pos": [
-13,
14
],
"end": [
-13,
-12
],
"line_type": "solid",
"line_thickness": 2.0,
"line_color": "#ffaa00ff"
},
"c4476a1b-7216-4eab-aad6-69ecb5af4b79": {
"layer": 6,
"type": "line",
"pos": [
-13,
-13
],
"end": [
4,
-13
],
"line_type": "solid",
"line_thickness": 2.0,
"line_color": "#ffaa00ff"
},
"2b69a1e2-0f80-4a1a-b31c-1f32b91bc46a": {
"layer": 7,
"type": "line",
"pos": [
4,
-13
],
"end": [
4,
14
],
"line_type": "solid",
"line_thickness": 2.0,
"line_color": "#ffaa00ff"
},
"7fac6744-2d5b-49a4-bfca-27cc8889a5b6": {
"layer": 8,
"type": "line",
"pos": [
4,
14
],
"end": [
21,
14
],
"line_type": "solid",
"line_thickness": 2.0,
"line_color": "#ffaa00ff"
},
"81390b87-3648-4556-a439-9456232781df": {
"layer": 9,
"type": "line",
"pos": [
21,
14
],
"end": [
21,
-13
],
"line_type": "solid",
"line_thickness": 2.0,
"line_color": "#ffaa00ff"
},
"c3c9eddf-f4c7-4f76-8bc5-7c313fc9ea72": {
"layer": 10,
"type": "line",
"pos": [
21,
-14
],
"end": [
32,
-14
],
"line_type": "solid",
"line_thickness": 2.0,
"line_color": "#ffaa00ff"
}
},
"port_positions": {
"4f9f6b29-36f0-460e-9fd9-d4bdf0ea3c80": [
-8,
-8
]
}
},
"ac215607-dd97-4142-b42f-f23502e51cfa": {
"shapes": {
"9336da30-7335-4eea-a8a2-ae69d8cad9e1": {
"layer": 1,
"type": "ellipse",
"pos": [
-36,
-36
],
"width": 72.0,
"height": 72.0,
"line_type": "solid",
"line_thickness": 8.0,
"line_color": "#00007fff",
"fill_color": "#ebebebff"
}
},
"port_positions": {
"baadcb27-0807-4366-819f-7396a1ed205d": [
-24,
9
],
"53cc98de-2e05-49ea-af26-e26d5dcd99e7": [
-24,
-26
],
"0574652d-df52-4798-8072-da0a26eee873": [
11,
-8
]
}
} }
} }
}, },
@@ -1207,8 +1888,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 +1914,38 @@
"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
],
"83dd8220-554c-4907-b713-c37e1f600f0d": [
-96,
648
],
"404e82f4-19e2-44ad-b409-3f021e483a39": [
228,
640
],
"2b0156f6-9977-4735-88dd-14f5eb3d87b5": [
232,
828
],
"927326ff-f73a-4f98-b7fd-aaf97989121e": [
-428,
552
],
"ac215607-dd97-4142-b42f-f23502e51cfa": [
-96,
636
],
"b3e059a3-4517-435d-aa63-ddfbbf6fa071": [
488,
552
] ]
}, },
"component_labels": { "component_labels": {
@@ -1343,6 +2056,17 @@
"dassl_tolerance": 1e-06 "dassl_tolerance": 1e-06
} }
} }
},
"port_metadata_database": {
"format_version": 1,
"ports": {
"53cc98de-2e05-49ea-af26-e26d5dcd99e7": {
"connection_annotation": "-"
},
"baadcb27-0807-4366-819f-7396a1ed205d": {
"connection_annotation": "+"
}
}
} }
} }
} }