diff --git a/examples/BondGraphs.beb b/examples/BondGraphs.beb
new file mode 100644
index 0000000..230332f
Binary files /dev/null and b/examples/BondGraphs.beb differ
diff --git a/lib/bondgraph.beb b/lib/bondgraph.beb
index c5a1137..c11be35 100644
Binary files a/lib/bondgraph.beb and b/lib/bondgraph.beb differ
diff --git a/lib/signal.beb b/lib/signal.beb
index a9971a5..6a8eb59 100644
Binary files a/lib/signal.beb and b/lib/signal.beb differ
diff --git a/lib/signal_sources.beb b/lib/signal_sources.beb
index 985ddd8..2012e71 100644
Binary files a/lib/signal_sources.beb and b/lib/signal_sources.beb differ
diff --git a/src/bedit_gui/commands/component_command.py b/src/bedit_gui/commands/component_command.py
index 915fd4b..ba14278 100644
--- a/src/bedit_gui/commands/component_command.py
+++ b/src/bedit_gui/commands/component_command.py
@@ -1,7 +1,9 @@
+from copy import deepcopy
+
from PySide6.QtGui import QUndoCommand
-from bedit_core.models import Component, ComponentID, Connection, ConnectionID, EquationImplementation, Graph, GraphImplementation, Interface
-from bedit_gui.models import Icon
+from bedit_core.models import Component, ComponentID, Connection, ConnectionID, EquationImplementation, Graph, GraphImplementation, Interface, PortID
+from bedit_gui.models import Icon, PortMetadata
class AddEmptyGraphComponent(QUndoCommand):
@@ -62,10 +64,16 @@ class DeleteComponent(QUndoCommand):
if connection.source in port_ids or connection.target in port_ids:
self.connections.append((index, connection_id, connection))
self.icons = {}
+ self.port_metadata = {}
for component_id in self._component_ids(self.component_id, component):
icon = document.stored_component_icon(component_id)
if icon is not None:
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:
if self.parent_graph is not None:
@@ -75,6 +83,11 @@ class DeleteComponent(QUndoCommand):
self.document.model_changed.emit(self.document.model)
for component_id in self.icons:
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:
self._restore_item(self.components, self.component_id, self.component, self.component_index)
@@ -84,6 +97,10 @@ class DeleteComponent(QUndoCommand):
self.document.model_changed.emit(self.document.model)
for component_id, icon in self.icons.items():
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
def _find_component(cls, components: dict[ComponentID, Component], target: Component, parent_graph: Graph | None = None) -> tuple[dict[ComponentID, Component], ComponentID, Graph | None]:
@@ -105,6 +122,14 @@ class DeleteComponent(QUndoCommand):
component_ids.extend(cls._component_ids(child_id, child))
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
def _restore_item(items: dict, item_id: object, item: object, index: int) -> None:
values = list(items.items())
@@ -114,7 +139,7 @@ class DeleteComponent(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")
self.document = document
self.target = target
@@ -122,6 +147,7 @@ class PasteComponents(QUndoCommand):
self.icons = icons
self.graph_id = graph_id
self.positions = positions or {}
+ self.port_metadata = deepcopy(port_metadata or {})
def redo(self) -> None:
self.target.update(self.components)
@@ -131,6 +157,10 @@ class PasteComponents(QUndoCommand):
self.document.model_changed.emit(self.document.model)
for component_id, icon in self.icons.items():
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:
for component_id in self.components:
@@ -141,3 +171,8 @@ class PasteComponents(QUndoCommand):
self.document.model_changed.emit(self.document.model)
for component_id in self.icons:
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)
diff --git a/src/bedit_gui/commands/port_metadata_command.py b/src/bedit_gui/commands/port_metadata_command.py
new file mode 100644
index 0000000..78fdd01
--- /dev/null
+++ b/src/bedit_gui/commands/port_metadata_command.py
@@ -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)
diff --git a/src/bedit_gui/controllers/clipboard_controller.py b/src/bedit_gui/controllers/clipboard_controller.py
index 473d102..ced6127 100644
--- a/src/bedit_gui/controllers/clipboard_controller.py
+++ b/src/bedit_gui/controllers/clipboard_controller.py
@@ -133,11 +133,11 @@ class DocumentTreeClipboardHandler(ClipboardHandler):
if target is None or payload is None:
return
try:
- components, icons = import_components(payload)
+ components, icons, port_metadata = import_components(payload)
except (TypeError, ValueError) as exc:
QMessageBox.critical(self.tree, "Could not paste components", str(exc))
return
- self.document.paste_components(target, components, icons)
+ self.document.paste_components(target, components, icons, port_metadata)
def delete(self) -> None:
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:
try:
- components, icons = import_components(payload)
+ components, icons, port_metadata = import_components(payload)
except (TypeError, ValueError) as exc:
QMessageBox.critical(self.editor, "Could not paste components", str(exc))
return
x, y = position
spacing = self.editor.snap_to_grid_size * 4
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:
components = self._selected_components()
diff --git a/src/bedit_gui/controllers/document_tree_controller.py b/src/bedit_gui/controllers/document_tree_controller.py
index f8dbd78..5205178 100644
--- a/src/bedit_gui/controllers/document_tree_controller.py
+++ b/src/bedit_gui/controllers/document_tree_controller.py
@@ -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 Document as CoreDocument
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.param_editor_dialog import ParamEditorDialog
from bedit_gui.views.icon_editor_window import IconEditorWindow
@@ -22,6 +22,7 @@ ICON_SIZE = QSize(16, 16)
class InterfaceEditorLike(Protocol):
def exec(self) -> int: ...
def ports(self) -> dict[PortID, Port]: ...
+ def port_metadata(self) -> dict[PortID, PortMetadata]: ...
class ParamEditorLike(Protocol):
def exec(self) -> int: ...
@@ -29,7 +30,7 @@ class ParamEditorLike(Protocol):
InterfaceEditorFactory = Callable[
- [dict[PortID, Port], MainWindow],
+ [dict[PortID, Port], dict[PortID, PortMetadata], MainWindow],
InterfaceEditorLike,
]
@@ -60,15 +61,17 @@ class DocumentTreeController(QObject):
window.ui.documentTree.setModel(self.model)
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.port_metadata_change_requested.connect(self._change_equation_port_metadata)
document.model_changed.connect(self._on_document_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_label_changed.connect(self._on_graph_component_label_changed)
document.graph_connection_points_changed.connect(self._on_graph_connection_points_changed)
document.equation_text_changed.connect(self._on_equation_text_changed)
self.model.rename_document_requested.connect(self.document.rename)
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_context_menu_requested.connect(self._show_graph_component_context_menu)
window.graph_editor.component_open_requested.connect(self._open_graph_component)
@@ -125,7 +128,8 @@ class DocumentTreeController(QObject):
def _show_component(self, component: Component | None) -> None:
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()
else:
self.window.equation_editor.set_component(None)
@@ -133,7 +137,8 @@ class DocumentTreeController(QObject):
if component is not None and isinstance(component.implementation, GraphImplementation):
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}
- 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()
else:
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:
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:
graph_component = self.window.graph_editor.component()
if graph_component is not None and self.document.component_id(graph_component) == graph_id:
@@ -242,10 +258,11 @@ class DocumentTreeController(QObject):
def _edit_interface(self, component: Component) -> None:
dialog = self.interface_editor_factory(
component.interface.ports,
+ {port_id: self.document.port_metadata(port_id) for port_id in component.interface.ports},
self.window,
)
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:
dialog = self.param_editor_factory(
diff --git a/src/bedit_gui/controllers/library_controller.py b/src/bedit_gui/controllers/library_controller.py
index 026664d..821dfe0 100644
--- a/src/bedit_gui/controllers/library_controller.py
+++ b/src/bedit_gui/controllers/library_controller.py
@@ -1,8 +1,8 @@
from PySide6.QtCore import QObject, QSize, Qt
from PySide6.QtWidgets import QAbstractItemView, QHeaderView
-from bedit_core.models import Component, ComponentID, GraphImplementation
-from bedit_gui.models import Icon, IconDatabase
+from bedit_core.models import Component, ComponentID, GraphImplementation, PortID
+from bedit_gui.models import Icon, IconDatabase, PortMetadata, PortMetadataDatabase
from bedit_gui.services.application_settings import ApplicationSettings
from bedit_gui.services.component_clipboard import export_component_data
from bedit_gui.services.libraries import load_library_documents
@@ -18,7 +18,7 @@ class LibraryController(QObject):
super().__init__(window)
self.window = window
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)
tree = window.ui.libraryTree
@@ -44,7 +44,9 @@ class LibraryController(QObject):
for library in libraries:
database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None
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])
for library in libraries:
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:
roots = {}
icons = {}
+ port_metadata = {}
for component in components:
source = self._component_sources.get(id(component))
if source is None:
continue
- component_id, source_icons = source
+ component_id, source_icons, source_port_metadata = source
roots[component_id] = component
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():
- self._component_sources[id(component)] = (component_id, icons)
+ self._component_sources[id(component)] = (component_id, icons, port_metadata)
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:
for component_id, component in components.items():
diff --git a/src/bedit_gui/documents/document.py b/src/bedit_gui/documents/document.py
index c823b7f..0f18c58 100644
--- a/src/bedit_gui/documents/document.py
+++ b/src/bedit_gui/documents/document.py
@@ -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_label_command import ChangeGraphComponentLabelCommand
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.rename_component_command import RenameComponentCommand
from bedit_gui.commands.rename_document_command import RenameDocumentCommand
from bedit_gui.commands.simulation_database_command import ChangeSimulationDatabaseCommand
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
@@ -35,6 +36,7 @@ class Document(QObject):
icon_changed = Signal(object, object)
equation_text_changed = Signal(object, str)
simulation_database_changed = Signal(object)
+ port_metadata_database_changed = Signal(object)
graph_component_position_changed = Signal(object, object, object)
graph_component_label_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)
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:
graph_id = self.component_id(graph_component)
database = self._graph_database(False)
@@ -147,6 +183,18 @@ class Document(QObject):
if graph is None or graph.component_positions.get(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:
if position is None:
database = self._graph_database(False)
@@ -309,7 +357,7 @@ class Document(QObject):
metadata["icon_database"] = 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
removed = [
RemovePortCommand(self, component, port_id)
@@ -325,12 +373,23 @@ class Document(QObject):
if current[port_id] != ports[port_id]
]
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
self.undo_stack.beginMacro("Edit interface")
for command in commands:
self.undo_stack.push(command)
+ if metadata_changed:
+ self.undo_stack.push(ChangePortMetadataDatabaseCommand(self, database))
self.undo_stack.endMacro()
@@ -396,16 +455,16 @@ class Document(QObject):
self.undo_stack.push(DeleteComponent(self, component))
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:
return
names = {component.name for component in target.values()}
for component in components.values():
component.name = self._unique_name(names, 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:
return
target = graph_component.implementation.graph.components
@@ -413,7 +472,7 @@ class Document(QObject):
for component in components.values():
component.name = self._unique_name(names, 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
def _unique_component_name(components: dict[ComponentID, Component], name: str) -> str:
diff --git a/src/bedit_gui/models.py b/src/bedit_gui/models.py
index ae37934..8834269 100644
--- a/src/bedit_gui/models.py
+++ b/src/bedit_gui/models.py
@@ -143,6 +143,31 @@ class IconDatabase:
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()}}
+@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
class GraphConnection:
points: list[tuple[int, int]] = field(default_factory=list)
diff --git a/src/bedit_gui/services/component_clipboard.py b/src/bedit_gui/services/component_clipboard.py
index 1c3fa6d..a14b114 100644
--- a/src/bedit_gui/services/component_clipboard.py
+++ b/src/bedit_gui/services/component_clipboard.py
@@ -6,7 +6,7 @@ from typing import Any
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_gui.documents import Document as GuiDocument
-from bedit_gui.models import Icon, ShapeID
+from bedit_gui.models import Icon, PortMetadata, ShapeID
FORMAT_VERSION = 1
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}
component_ids = _all_component_ids(roots)
icons = {}
+ port_metadata = {}
for component_id in component_ids:
icon = document.stored_component_icon(component_id)
if icon is not None:
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))
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}
- 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":
raise ValueError("unsupported component clipboard format")
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.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
- 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]:
@@ -87,3 +97,12 @@ def _all_component_ids(components: dict[ComponentID, Component]) -> list[Compone
if isinstance(component.implementation, GraphImplementation):
component_ids.extend(_all_component_ids(component.implementation.graph.components))
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
diff --git a/src/bedit_gui/services/document_files.py b/src/bedit_gui/services/document_files.py
index 8bdc676..cbafac2 100644
--- a/src/bedit_gui/services/document_files.py
+++ b/src/bedit_gui/services/document_files.py
@@ -6,7 +6,7 @@ from pathlib import Path
from bedit_core.models import Document
from bedit_core.serialization import load as load_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:
@@ -18,6 +18,8 @@ def load(path: str | Path) -> Document:
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):
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
@@ -30,4 +32,6 @@ def save(document: Document, path: str | Path) -> None:
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):
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)
diff --git a/src/bedit_gui/ui/forms/port_editor_widget.ui b/src/bedit_gui/ui/forms/port_editor_widget.ui
index 869d71f..da3fe60 100644
--- a/src/bedit_gui/ui/forms/port_editor_widget.ui
+++ b/src/bedit_gui/ui/forms/port_editor_widget.ui
@@ -148,6 +148,20 @@
+ -
+
+
+ Connection annotation:
+
+
+
+ -
+
+
+ For example, + or -
+
+
+
-
diff --git a/src/bedit_gui/ui/forms/port_editor_widget_ui.py b/src/bedit_gui/ui/forms/port_editor_widget_ui.py
new file mode 100644
index 0000000..0b33c33
--- /dev/null
+++ b/src/bedit_gui/ui/forms/port_editor_widget_ui.py
@@ -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
+
diff --git a/src/bedit_gui/views/dialogs/interface_editor_dialog.py b/src/bedit_gui/views/dialogs/interface_editor_dialog.py
index 2877f10..82f7747 100644
--- a/src/bedit_gui/views/dialogs/interface_editor_dialog.py
+++ b/src/bedit_gui/views/dialogs/interface_editor_dialog.py
@@ -8,6 +8,7 @@ from PySide6.QtWidgets import (
)
from bedit_core.models import Port, PortID
+from bedit_gui.models import PortMetadata
from bedit_gui.views.port_editor_widget import PortEditorWidget
@@ -17,6 +18,7 @@ class InterfaceEditorDialog(QDialog):
def __init__(
self,
ports: dict[PortID, Port],
+ port_metadata: dict[PortID, PortMetadata] | None = None,
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
@@ -25,7 +27,7 @@ class InterfaceEditorDialog(QDialog):
layout = QVBoxLayout(self)
self.editor = PortEditorWidget(self)
- self.editor.set_ports(ports)
+ self.editor.set_ports(ports, port_metadata)
layout.addWidget(self.editor)
buttons = QDialogButtonBox(
@@ -38,3 +40,6 @@ class InterfaceEditorDialog(QDialog):
def ports(self) -> dict[PortID, Port]:
return self.editor.ports()
+
+ def port_metadata(self) -> dict[PortID, PortMetadata]:
+ return self.editor.port_metadata()
diff --git a/src/bedit_gui/views/equation_editor_widget.py b/src/bedit_gui/views/equation_editor_widget.py
index 0ae8a6f..481f183 100644
--- a/src/bedit_gui/views/equation_editor_widget.py
+++ b/src/bedit_gui/views/equation_editor_widget.py
@@ -3,7 +3,8 @@ from __future__ import annotations
from PySide6.QtCore import QEvent, QObject, QTimer, Qt, Signal
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.views.param_editor_widget import ParamEditorWidget
from bedit_gui.views.port_editor_widget import PortEditorWidget
@@ -14,6 +15,7 @@ class EquationEditorWidget(QWidget):
component_changed = Signal(object)
equation_text_change_requested = Signal(object, str, object, int)
+ port_metadata_change_requested = Signal(object, object)
sidebar_visible_changed = Signal(bool)
def __init__(self, parent: QWidget | None = None) -> None:
@@ -22,6 +24,7 @@ class EquationEditorWidget(QWidget):
self.ui = Ui_equationEditorWidget()
self.ui.setupUi(self)
self._component: Component | None = None
+ self._port_metadata: dict[PortID, PortMetadata] = {}
self._loading = False
self._edit_id = 0
self._edit_timer = QTimer(self)
@@ -45,11 +48,12 @@ class EquationEditorWidget(QWidget):
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):
raise TypeError("EquationEditorWidget only supports components with an equation implementation")
self._component = component
+ self._port_metadata = port_metadata or {}
self.refresh()
def component(self) -> Component | None:
@@ -88,7 +92,7 @@ class EquationEditorWidget(QWidget):
self.ui.initialEquationsTextEdit.setPlainText("\n".join(implementation.initial_equations))
self.ui.equationsTextEdit.setPlainText("\n".join(implementation.equations))
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._set_editors_enabled(component is not None)
@@ -145,8 +149,21 @@ class EquationEditorWidget(QWidget):
def _ports_changed(self) -> None:
if self._loading or self._component is None:
return
- self._component.interface.ports = self.port_editor.ports()
- self.component_changed.emit(self._component)
+ ports = self.port_editor.ports()
+ 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:
self.ui.verticalLayout_2.replaceWidget(placeholder, editor)
diff --git a/src/bedit_gui/views/graph_editor_widget.py b/src/bedit_gui/views/graph_editor_widget.py
index d0e8c8e..eb67ad7 100644
--- a/src/bedit_gui/views/graph_editor_widget.py
+++ b/src/bedit_gui/views/graph_editor_widget.py
@@ -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 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.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
@@ -34,6 +34,9 @@ ARROW_HALF_WIDTH = 16.0
SIGNAL_ARROW_LENGTH = ARROW_LENGTH / 2
SIGNAL_ARROW_HALF_WIDTH = ARROW_HALF_WIDTH / 2
CAUSALITY_TICK_HALF_LENGTH = 16.0
+CONNECTION_ANNOTATION_FONT_SIZE = 18.0
+CONNECTION_ANNOTATION_BACK_OFFSET = 24.0
+CONNECTION_ANNOTATION_SIDE_OFFSET = 14.0
class GraphEditorMode(Enum):
@@ -152,7 +155,7 @@ class GraphComponentItem(QGraphicsPixmapItem):
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
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
value = QPointF(round(value.x() / grid_size) * grid_size, round(value.y() / grid_size) * grid_size)
result = super().itemChange(change, value)
@@ -167,6 +170,7 @@ class GraphComponentItem(QGraphicsPixmapItem):
self._drag_start = QPointF(self.pos())
self._dragging = True
super().mousePressEvent(event)
+ self.editor.begin_component_move()
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self.editor.mode is GraphEditorMode.CONNECTION:
@@ -176,10 +180,7 @@ class GraphComponentItem(QGraphicsPixmapItem):
return
super().mouseReleaseEvent(event)
self._dragging = False
- position = (round(self.pos().x()), round(self.pos().y()))
- self.setPos(*position)
- if self.pos() != self._drag_start:
- self.editor.finish_component_move(self.component_id, position)
+ self.editor.finish_component_moves()
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if event.button() == Qt.MouseButton.LeftButton:
@@ -295,7 +296,7 @@ class GraphConnectionPointItem(QGraphicsEllipseItem):
class GraphEditorWidget(QWidget):
- component_move_requested = Signal(object, object, object)
+ component_moves_requested = Signal(object, object)
component_context_menu_requested = Signal(object, object)
component_open_requested = Signal(object)
component_label_move_requested = Signal(object, object, object)
@@ -312,14 +313,17 @@ class GraphEditorWidget(QWidget):
self._component: Component | None = None
self._graph = Graph()
self._icons: dict[ComponentID, Icon] = {}
+ self._port_metadata: dict[PortID, PortMetadata] = {}
self._mode = GraphEditorMode.NORMAL
self._connection_start: ComponentID | None = None
self._connection_preview: QGraphicsPathItem | None = None
self._component_items: dict[ComponentID, GraphComponentItem] = {}
+ self._component_drag_starts: dict[ComponentID, tuple[int, int]] = {}
self._component_bounds: dict[ComponentID, QRectF] = {}
self._component_label_items: dict[ComponentID, GraphComponentLabelItem] = {}
self._connection_items: dict[ConnectionID, GraphConnectionItem] = {}
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.scene = GraphGraphicsScene(self)
self.scene.setSceneRect(-SCENE_SIZE / 2, -SCENE_SIZE / 2, SCENE_SIZE, SCENE_SIZE)
@@ -364,12 +368,12 @@ class GraphEditorWidget(QWidget):
self._clear_connection_start()
component = self._component
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:
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):
raise TypeError("GraphEditorWidget only supports components with a graph implementation")
component_changed = component is not self._component
@@ -377,11 +381,14 @@ class GraphEditorWidget(QWidget):
self._component = component
self._graph = graph or Graph()
self._icons = icons or {}
+ self._port_metadata = port_metadata or {}
+ self._component_drag_starts = {}
self._component_items = {}
self._component_bounds = {}
self._component_label_items = {}
self._connection_items = {}
self._connection_point_items = {}
+ self._connection_annotation_items = {}
self.scene.clear()
if component is None:
return
@@ -420,6 +427,17 @@ class GraphEditorWidget(QWidget):
connection_item.setData(0, str(connection_id))
self._connection_items[connection_id] = 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)
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()
@@ -452,6 +470,23 @@ class GraphEditorWidget(QWidget):
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
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:
points = self._connection_metadata_points(connection_id)
@@ -477,6 +512,9 @@ class GraphEditorWidget(QWidget):
connection_item = self._connection_items.pop(connection_id, None)
if connection_item is not None:
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
self._graph.connections[connection_id] = GraphConnection(points=list(points))
interior_points = points[1:-1]
@@ -532,9 +570,23 @@ class GraphEditorWidget(QWidget):
best_distance = distance
return best_index
- def finish_component_move(self, component_id: ComponentID, position: tuple[int, int]) -> None:
- if self._component is not None:
- self.component_move_requested.emit(self._component, component_id, position)
+ def begin_component_move(self) -> 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)}
+
+ 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:
if self._component is not None:
diff --git a/src/bedit_gui/views/port_editor_widget.py b/src/bedit_gui/views/port_editor_widget.py
index 90a64d7..a9a45ff 100644
--- a/src/bedit_gui/views/port_editor_widget.py
+++ b/src/bedit_gui/views/port_editor_widget.py
@@ -7,6 +7,7 @@ from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import QButtonGroup, QLayout, QWidget
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
@@ -21,6 +22,7 @@ class PortEditorWidget(QWidget):
self.ui = Ui_PortEditor()
self.ui.setupUi(self)
self._ports: dict[PortID, Port] = {}
+ self._port_metadata: dict[PortID, PortMetadata] = {}
self._port_ids: list[PortID] = []
self._loading = False
@@ -54,6 +56,7 @@ class PortEditorWidget(QWidget):
self.ui.widthSize.valueChanged.connect(self._form_changed)
self.ui.heightSize.valueChanged.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.domainComboBox.currentTextChanged.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._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._port_metadata = deepcopy(port_metadata or {})
self._port_ids = list(self._ports)
self._rebuild_list()
def ports(self) -> dict[PortID, Port]:
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:
self._list_model.clear()
for port_id in self._port_ids:
@@ -90,7 +97,7 @@ class PortEditorWidget(QWidget):
self.ui.removePort.setEnabled(port_id is not None)
self._set_editor_enabled(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:
index = self.ui.portList.currentIndex()
@@ -98,7 +105,7 @@ class PortEditorWidget(QWidget):
return None
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.ui.nameEdit.setText(port.name)
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.heightSize.setValue(port.matrix_size[1])
self.ui.multiplicityCheckBox.setChecked(port.multiplicity)
+ self.ui.connectionAnnotationEdit.setText(metadata.connection_annotation or "")
self.ui.descriptionEdit.setPlainText(port.description or "")
if isinstance(port, SignalPort):
@@ -135,6 +143,12 @@ class PortEditorWidget(QWidget):
old_port = self._ports[port_id]
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._ports[port_id].name
)
@@ -188,6 +202,7 @@ class PortEditorWidget(QWidget):
return
row = self._port_ids.index(port_id)
del self._ports[port_id]
+ self._port_metadata.pop(port_id, None)
self._port_ids.remove(port_id)
selected = (
self._port_ids[min(row, len(self._port_ids) - 1)]
@@ -201,6 +216,9 @@ class PortEditorWidget(QWidget):
signal = self.ui.typeSignal.isChecked()
self._set_layout_visible(self.ui.signalOptions, 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
def _set_layout_visible(layout: QLayout, visible: bool) -> None:
diff --git a/untitled.bedit.json b/untitled.bedit.json
index 2fea8ac..6e8be13 100644
--- a/untitled.bedit.json
+++ b/untitled.bedit.json
@@ -608,11 +608,219 @@
]
}
},
- "ac5011ce-4659-4bdd-900a-63a1c779652c": {
+ "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": {
- "69974610-3bd6-42c1-a8bb-afe6086ef0f3": {
+ "4f9f6b29-36f0-460e-9fd9-d4bdf0ea3c80": {
"port_type": "signal",
"name": "y",
"direction": "output",
@@ -629,7 +837,7 @@
}
},
"parameters": {
- "8e128f7a-cf97-4f27-a664-488921819395": {
+ "b6460a26-0b89-49e0-8ec7-99281a66668b": {
"name": "amplitude",
"value": "1.0",
"value_type": "real",
@@ -641,7 +849,7 @@
"unit": null,
"description": null
},
- "5ff17831-f2f9-4833-9e4b-4facf7c28c0c": {
+ "983d6c65-8e81-4f49-9faa-d024e8e6a9d3": {
"name": "width",
"value": "50.0",
"value_type": "real",
@@ -653,7 +861,7 @@
"unit": null,
"description": "Pulse width in %"
},
- "b036592b-4dc0-498b-a966-7146cf9cf95b": {
+ "c9ccd873-f83a-4b08-8827-4a88a06d502a": {
"name": "offset",
"value": "0",
"value_type": "real",
@@ -665,7 +873,7 @@
"unit": null,
"description": null
},
- "df82150d-973d-4a8a-9bd5-6ce21a2e8bbb": {
+ "9cd46f38-6ddb-4d69-946d-df96e9f30078": {
"name": "period",
"value": "1.0",
"value_type": "real",
@@ -677,7 +885,7 @@
"unit": null,
"description": null
},
- "1195c93e-3309-494e-9a56-96e8fd5e330c": {
+ "4e1d29eb-48c8-479a-8968-8f57fd95ea9f": {
"name": "nperiod",
"value": "-1",
"value_type": "int",
@@ -689,7 +897,7 @@
"unit": null,
"description": "Number of periods (<0 means infinite)"
},
- "3f5f7b5c-2794-4e75-a5c8-1ef2b0ecd87d": {
+ "68102f0d-b58b-4c10-af0d-dffae406fb07": {
"name": "startTime",
"value": "0.0",
"value_type": "real",
@@ -723,11 +931,11 @@
]
}
},
- "ec0599cb-b57b-4194-b166-526d5cd5382a": {
+ "ac215607-dd97-4142-b42f-f23502e51cfa": {
"name": "plusminus",
"interface": {
"ports": {
- "23a8b215-7c2f-4ccf-b020-4d274d7a228f": {
+ "baadcb27-0807-4366-819f-7396a1ed205d": {
"port_type": "signal",
"name": "plus",
"direction": "input",
@@ -741,7 +949,7 @@
"quantity": null,
"unit": null
},
- "4c4771f8-22c8-4829-a060-913470e2b2fc": {
+ "53cc98de-2e05-49ea-af26-e26d5dcd99e7": {
"port_type": "signal",
"name": "minus",
"direction": "input",
@@ -755,7 +963,7 @@
"quantity": null,
"unit": null
},
- "a186a0e1-7aea-4fb6-871c-0ba70d78b8ac": {
+ "0574652d-df52-4798-8072-da0a26eee873": {
"port_type": "signal",
"name": "y",
"direction": "output",
@@ -835,15 +1043,25 @@
"source": "a2709c23-7485-43b2-bc87-28a0f4c37c0b",
"target": "8bd1cc42-03b9-4c46-b31b-c1f46933dc9e"
},
- "42f8d11c-8b31-476a-8e56-dd1ac3703508": {
+ "122eaf51-7baf-457f-a944-f946b6ab321a": {
"connection_type": "signal",
- "source": "69974610-3bd6-42c1-a8bb-afe6086ef0f3",
- "target": "23a8b215-7c2f-4ccf-b020-4d274d7a228f"
+ "source": "4f9f6b29-36f0-460e-9fd9-d4bdf0ea3c80",
+ "target": "baadcb27-0807-4366-819f-7396a1ed205d"
},
- "81e6b0d7-65f2-4edc-a303-6eee78ac1972": {
+ "8b1c34a9-03c2-4bb5-81ab-7fa66bcafe8e": {
"connection_type": "signal",
"source": "5807dc83-d049-4f0f-9708-d041a752fcd0",
- "target": "4c4771f8-22c8-4829-a060-913470e2b2fc"
+ "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"
}
}
}
@@ -1333,9 +1551,9 @@
]
}
},
- "ac5011ce-4659-4bdd-900a-63a1c779652c": {
+ "404e82f4-19e2-44ad-b409-3f021e483a39": {
"shapes": {
- "7a9115fe-4e40-49d9-ab50-2f399ca1038b": {
+ "29dafe3d-8392-49c0-98ee-80e941b32a52": {
"layer": 0,
"type": "rectangle",
"pos": [
@@ -1350,7 +1568,95 @@
"line_color": "#00007fff",
"fill_color": "#ebebebff"
},
- "80e150dc-07d3-4d27-9fe0-eecb82fb2689": {
+ "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": [
@@ -1365,7 +1671,7 @@
"line_thickness": 3.0,
"line_color": "#000000ff"
},
- "66c7fb78-2866-4f9f-9df2-95cfa4c57a2d": {
+ "94fc1429-0ef2-403b-aa5d-7edcb7b3ebf2": {
"layer": 2,
"type": "line",
"pos": [
@@ -1380,7 +1686,7 @@
"line_thickness": 3.0,
"line_color": "#000000ff"
},
- "b1b1f425-eea6-48e7-a61d-f9eb221984b7": {
+ "0d82b577-e3fb-4434-adfb-883e1918e30f": {
"layer": 3,
"type": "line",
"pos": [
@@ -1388,79 +1694,138 @@
14
],
"end": [
- 3,
+ -14,
14
],
"line_type": "solid",
"line_thickness": 2.0,
"line_color": "#ffaa00ff"
},
- "0c0b7ec3-557d-4fb4-b8b8-b6b6a269ec55": {
+ "531dcbea-2c2d-4ed9-bcbc-dfc9b0145527": {
"layer": 5,
"type": "line",
"pos": [
- 3,
+ -13,
14
],
"end": [
- 3,
+ -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"
},
- "e5c99d8a-69db-4915-b1e7-84ac5e52de02": {
- "layer": 6,
+ "2b69a1e2-0f80-4a1a-b31c-1f32b91bc46a": {
+ "layer": 7,
"type": "line",
"pos": [
- 3,
+ 4,
-13
],
"end": [
- 32,
+ 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": {
- "69974610-3bd6-42c1-a8bb-afe6086ef0f3": [
+ "4f9f6b29-36f0-460e-9fd9-d4bdf0ea3c80": [
-8,
-8
]
}
},
- "ec0599cb-b57b-4194-b166-526d5cd5382a": {
+ "ac215607-dd97-4142-b42f-f23502e51cfa": {
"shapes": {
- "ecbbad0e-67e3-4d13-ba25-7660f50bc38e": {
- "layer": 0,
- "type": "rectangle",
+ "9336da30-7335-4eea-a8a2-ae69d8cad9e1": {
+ "layer": 1,
+ "type": "ellipse",
"pos": [
- -48,
- -48
+ -36,
+ -36
],
- "width": 96.0,
- "height": 96.0,
+ "width": 72.0,
+ "height": 72.0,
"line_type": "solid",
- "line_thickness": 5.0,
- "corner_radius": 8.0,
+ "line_thickness": 8.0,
"line_color": "#00007fff",
"fill_color": "#ebebebff"
}
},
"port_positions": {
- "23a8b215-7c2f-4ccf-b020-4d274d7a228f": [
+ "baadcb27-0807-4366-819f-7396a1ed205d": [
-24,
9
],
- "4c4771f8-22c8-4829-a060-913470e2b2fc": [
+ "53cc98de-2e05-49ea-af26-e26d5dcd99e7": [
-24,
-26
],
- "a186a0e1-7aea-4fb6-871c-0ba70d78b8ac": [
+ "0574652d-df52-4798-8072-da0a26eee873": [
11,
-8
]
@@ -1557,6 +1922,30 @@
"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": {
@@ -1667,6 +2056,17 @@
"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": "+"
+ }
+ }
}
}
}