diff --git a/src/bedit_gui/commands/port_commands.py b/src/bedit_gui/commands/port_commands.py new file mode 100644 index 0000000..b57fb0c --- /dev/null +++ b/src/bedit_gui/commands/port_commands.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from copy import deepcopy + +from PySide6.QtGui import QUndoCommand + +from bedit_core.models import Component, Port, PortID + + +class AddPortCommand(QUndoCommand): + def __init__( + self, + document: object, + component: Component, + port_id: PortID, + port: Port, + ) -> None: + super().__init__("Add port") + self.document = document + self.component = component + self.port_id = port_id + self.port = deepcopy(port) + + def redo(self) -> None: + self.component.interface.ports[self.port_id] = deepcopy(self.port) + self.document.model_changed.emit(self.document.model) + + def undo(self) -> None: + del self.component.interface.ports[self.port_id] + self.document.model_changed.emit(self.document.model) + + +class RemovePortCommand(QUndoCommand): + def __init__( + self, + document: object, + component: Component, + port_id: PortID, + ) -> None: + super().__init__("Remove port") + self.document = document + self.component = component + self.port_id = port_id + self.port = deepcopy(component.interface.ports[port_id]) + + def redo(self) -> None: + del self.component.interface.ports[self.port_id] + self.document.model_changed.emit(self.document.model) + + def undo(self) -> None: + self.component.interface.ports[self.port_id] = deepcopy(self.port) + self.document.model_changed.emit(self.document.model) + + +class ChangePortCommand(QUndoCommand): + def __init__( + self, + document: object, + component: Component, + port_id: PortID, + port: Port, + ) -> None: + super().__init__("Change port") + self.document = document + self.component = component + self.port_id = port_id + self.old_port = deepcopy(component.interface.ports[port_id]) + self.new_port = deepcopy(port) + + def redo(self) -> None: + self.component.interface.ports[self.port_id] = deepcopy(self.new_port) + self.document.model_changed.emit(self.document.model) + + def undo(self) -> None: + self.component.interface.ports[self.port_id] = deepcopy(self.old_port) + self.document.model_changed.emit(self.document.model) diff --git a/src/bedit_gui/controllers/document_tree_controller.py b/src/bedit_gui/controllers/document_tree_controller.py index 157b2f7..2073b5a 100644 --- a/src/bedit_gui/controllers/document_tree_controller.py +++ b/src/bedit_gui/controllers/document_tree_controller.py @@ -1,18 +1,44 @@ -from PySide6.QtCore import QObject +from collections.abc import Callable +from typing import Protocol +from PySide6.QtCore import QObject, QPoint, Qt +from PySide6.QtWidgets import QDialog, QMenu + +from bedit_core.models import Component, Port, PortID from bedit_core.models import Document as CoreDocument from bedit_gui.documents import Document +from bedit_gui.views.dialogs.interface_editor_dialog import ( + InterfaceEditorDialog, +) from bedit_gui.views.main_window import MainWindow from bedit_gui.views.models.document_tree_model import DocumentTreeModel +class InterfaceEditorLike(Protocol): + def exec(self) -> int: ... + + def ports(self) -> dict[PortID, Port]: ... + + +InterfaceEditorFactory = Callable[ + [dict[PortID, Port], MainWindow], + InterfaceEditorLike, +] + + class DocumentTreeController(QObject): - def __init__(self,document: Document,window: MainWindow) -> None: + def __init__( + self, + document: Document, + window: MainWindow, + interface_editor_factory: InterfaceEditorFactory = InterfaceEditorDialog, + ) -> None: super().__init__(window) self.document = document self.window = window self.model = DocumentTreeModel() + self.interface_editor_factory = interface_editor_factory window.ui.documentTree.setModel(self.model) document.model_changed.connect(self._on_document_changed) @@ -20,6 +46,12 @@ class DocumentTreeController(QObject): self.model.rename_component_requested.connect(self.document.rename_component) window.ui.documentTree.setHeaderHidden(True) + window.ui.documentTree.setContextMenuPolicy( + Qt.ContextMenuPolicy.CustomContextMenu + ) + window.ui.documentTree.customContextMenuRequested.connect( + self._show_context_menu + ) self._on_document_changed(document.model) @@ -30,3 +62,25 @@ class DocumentTreeController(QObject): # Optional presentation behavior. Later, you could instead remember # expanded component IDs and restore only those nodes. self.window.ui.documentTree.expandAll() + + def _show_context_menu(self, position: QPoint) -> None: + index = self.window.ui.documentTree.indexAt(position) + component = self.model.value(index) + if not isinstance(component, Component): + return + + menu = QMenu(self.window.ui.documentTree) + edit_interface = menu.addAction("Edit Interface") + selected = menu.exec( + self.window.ui.documentTree.viewport().mapToGlobal(position) + ) + if selected is edit_interface: + self._edit_interface(component) + + def _edit_interface(self, component: Component) -> None: + dialog = self.interface_editor_factory( + component.interface.ports, + self.window, + ) + if dialog.exec() == QDialog.DialogCode.Accepted: + self.document.update_component_ports(component, dialog.ports()) diff --git a/src/bedit_gui/documents/document.py b/src/bedit_gui/documents/document.py index f1060be..49a8011 100644 --- a/src/bedit_gui/documents/document.py +++ b/src/bedit_gui/documents/document.py @@ -5,11 +5,12 @@ from pathlib import Path from PySide6.QtCore import QObject, Signal from PySide6.QtGui import QUndoStack -from bedit_core.models import ID -from bedit_core.models import Document as CoreDocument, Component -from bedit_gui.services import document_files -from bedit_gui.commands.rename_document_command import RenameDocumentCommand +from bedit_core.models import ID, Component, Port, PortID +from bedit_core.models import Document as CoreDocument +from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand from bedit_gui.commands.rename_component_command import RenameComponentCommand +from bedit_gui.commands.rename_document_command import RenameDocumentCommand +from bedit_gui.services import document_files class Document(QObject): @@ -84,5 +85,33 @@ class Document(QObject): def rename(self, name: str) -> None: self.undo_stack.push(RenameDocumentCommand(self, name)) - def rename_component(self, component: Component, name:str) -> None: + def rename_component(self, component: Component, name: str) -> None: self.undo_stack.push(RenameComponentCommand(self, component, name)) + + def update_component_ports( + self, + component: Component, + ports: dict[PortID, Port], + ) -> None: + current = component.interface.ports + removed = [ + RemovePortCommand(self, component, port_id) + for port_id in current.keys() - ports.keys() + ] + added = [ + AddPortCommand(self, component, port_id, ports[port_id]) + for port_id in ports.keys() - current.keys() + ] + changed = [ + ChangePortCommand(self, component, port_id, ports[port_id]) + for port_id in current.keys() & ports.keys() + if current[port_id] != ports[port_id] + ] + commands = [*removed, *added, *changed] + if not commands: + return + + self.undo_stack.beginMacro("Edit interface") + for command in commands: + self.undo_stack.push(command) + self.undo_stack.endMacro() diff --git a/src/bedit_gui/ui/forms/port_editor_widget.ui b/src/bedit_gui/ui/forms/port_editor_widget.ui index e7677be..869d71f 100644 --- a/src/bedit_gui/ui/forms/port_editor_widget.ui +++ b/src/bedit_gui/ui/forms/port_editor_widget.ui @@ -7,7 +7,7 @@ 0 0 541 - 593 + 420 @@ -183,8 +183,31 @@ + + + + Causality: + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + @@ -194,6 +217,9 @@ + + QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow + @@ -204,7 +230,7 @@ - + 0 0 @@ -215,6 +241,12 @@ 20 + + + 16777215 + 60 + + diff --git a/src/bedit_gui/views/dialogs/interface_editor_dialog.py b/src/bedit_gui/views/dialogs/interface_editor_dialog.py new file mode 100644 index 0000000..2877f10 --- /dev/null +++ b/src/bedit_gui/views/dialogs/interface_editor_dialog.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from PySide6.QtWidgets import ( + QDialog, + QDialogButtonBox, + QVBoxLayout, + QWidget, +) + +from bedit_core.models import Port, PortID +from bedit_gui.views.port_editor_widget import PortEditorWidget + + +class InterfaceEditorDialog(QDialog): + """Modal wrapper around the reusable port editor widget.""" + + def __init__( + self, + ports: dict[PortID, Port], + parent: QWidget | None = None, + ) -> None: + super().__init__(parent) + self.setWindowTitle("Edit Interface") + self.resize(700, 500) + + layout = QVBoxLayout(self) + self.editor = PortEditorWidget(self) + self.editor.set_ports(ports) + layout.addWidget(self.editor) + + buttons = QDialogButtonBox( + QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, + self, + ) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + + def ports(self) -> dict[PortID, Port]: + return self.editor.ports() diff --git a/src/bedit_gui/views/models/document_tree_model.py b/src/bedit_gui/views/models/document_tree_model.py index d9b75cd..2c54869 100644 --- a/src/bedit_gui/views/models/document_tree_model.py +++ b/src/bedit_gui/views/models/document_tree_model.py @@ -4,7 +4,8 @@ from dataclasses import dataclass from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal -from bedit_core.models import Document as CoreDocument, ComponentID, Component, GraphImplementation +from bedit_core.models import Component, ComponentID, GraphImplementation +from bedit_core.models import Document as CoreDocument @dataclass @@ -36,7 +37,7 @@ class DocumentTreeModel(QAbstractItemModel): def columnCount(self, _parent: QModelIndex | None = None) -> int: return 1 - def index(self,row: int,column: int,parent: QModelIndex | None = None) -> QModelIndex: + def index(self, row: int, column: int, parent: QModelIndex | None = None) -> QModelIndex: parent_node = self._node(parent) if column != 0 or row < 0 or row >= len(parent_node.children): return QModelIndex() @@ -60,7 +61,7 @@ class DocumentTreeModel(QAbstractItemModel): row = grandparent.children.index(parent_node) return self.createIndex(row, 0, parent_node) - def data(self,index: QModelIndex,role: int = Qt.ItemDataRole.DisplayRole) -> object | None: + def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> object | None: if not index.isValid(): return None @@ -70,10 +71,10 @@ class DocumentTreeModel(QAbstractItemModel): if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): return node.name - + return None - def setData(self,index: QModelIndex,value: object,role: int = Qt.ItemDataRole.EditRole) -> bool: + def setData(self, index: QModelIndex, value: object, role: int = Qt.ItemDataRole.EditRole) -> bool: if role != Qt.ItemDataRole.EditRole or not index.isValid(): return False @@ -99,6 +100,12 @@ class DocumentTreeModel(QAbstractItemModel): node = index.internalPointer() return node if isinstance(node, DocumentTreeNode) else self._root + def value(self, index: QModelIndex) -> object | None: + if not index.isValid(): + return None + node = index.internalPointer() + return node.value if isinstance(node, DocumentTreeNode) else None + def flags(self, index: QModelIndex) -> Qt.ItemFlag: flags = super().flags(index) @@ -108,8 +115,7 @@ class DocumentTreeModel(QAbstractItemModel): node = index.internalPointer() # Make the document root node editable - if (isinstance(node, DocumentTreeNode) - and (isinstance(node.value, CoreDocument) or isinstance(node.value, Component))): + if isinstance(node, DocumentTreeNode) and isinstance(node.value, (CoreDocument, Component)): flags |= Qt.ItemFlag.ItemIsEditable return flags @@ -123,26 +129,17 @@ class DocumentTreeModel(QAbstractItemModel): children=[], ) # Add itself as a child so the document root is visible in the tree - document_root = DocumentTreeNode( - name = document.name, - value = document, - parent = root, - children = [] - ) + document_root = DocumentTreeNode(name=document.name, value=document, parent=root, children=[]) root.children.append(document_root) def _list_children(root: DocumentTreeNode, components: dict[ComponentID, Component]) -> None: - for component_id, component in components.items(): - component_node = DocumentTreeNode( - name = component.name, - value = component, - parent = root, - children = [] - ) + for component in components.values(): + component_node = DocumentTreeNode(name=component.name, value=component, parent=root, children=[]) root.children.append(component_node) if isinstance(component.implementation, GraphImplementation): _list_children(component_node, component.implementation.graph.components) + _list_children(document_root, document.root) return root diff --git a/src/bedit_gui/views/port_editor_widget.py b/src/bedit_gui/views/port_editor_widget.py new file mode 100644 index 0000000..90a64d7 --- /dev/null +++ b/src/bedit_gui/views/port_editor_widget.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +from copy import deepcopy + +from PySide6.QtCore import QItemSelection, Signal +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.ui.generated.ui_port_editor_widget import Ui_PortEditor + + +class PortEditorWidget(QWidget): + """Reusable editor for a detached draft of an interface's ports.""" + + ports_changed = Signal() + + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + + self.ui = Ui_PortEditor() + self.ui.setupUi(self) + self._ports: dict[PortID, Port] = {} + self._port_ids: list[PortID] = [] + self._loading = False + + self._type_group = QButtonGroup(self) + self._type_group.addButton(self.ui.typeSignal) + self._type_group.addButton(self.ui.typeBond) + self._orientation_group = QButtonGroup(self) + self._orientation_group.addButton(self.ui.inputOrientation) + self._orientation_group.addButton(self.ui.outputOrientation) + + self.ui.signalTypeComboBox.addItem("Real", ValueType.REAL) + self.ui.signalTypeComboBox.addItem("Integer", ValueType.INT) + self.ui.signalTypeComboBox.addItem("Boolean", ValueType.BOOL) + self.ui.domainComboBox.setEditable(True) + for causality in PortCausality: + self.ui.causalityComboBox.addItem(causality.value.replace("_", " ").title(), causality) + + self._list_model = QStandardItemModel(self) + self.ui.portList.setModel(self._list_model) + self.ui.portList.selectionModel().selectionChanged.connect( + self._selection_changed + ) + self.ui.addPort.clicked.connect(self._add_port) + self.ui.removePort.clicked.connect(self._remove_port) + + self.ui.nameEdit.textEdited.connect(self._form_changed) + self.ui.typeSignal.toggled.connect(self._form_changed) + self.ui.typeBond.toggled.connect(self._form_changed) + self.ui.inputOrientation.toggled.connect(self._form_changed) + self.ui.outputOrientation.toggled.connect(self._form_changed) + 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.signalTypeComboBox.currentIndexChanged.connect(self._form_changed) + self.ui.domainComboBox.currentTextChanged.connect(self._form_changed) + self.ui.causalityComboBox.currentIndexChanged.connect(self._form_changed) + self.ui.descriptionEdit.textChanged.connect(self._form_changed) + + self._set_editor_enabled(False) + self._update_option_visibility() + + def set_ports(self, ports: dict[PortID, Port]) -> None: + self._ports = deepcopy(ports) + self._port_ids = list(self._ports) + self._rebuild_list() + + def ports(self) -> dict[PortID, Port]: + return deepcopy(self._ports) + + def _rebuild_list(self, selected_id: PortID | None = None) -> None: + self._list_model.clear() + for port_id in self._port_ids: + self._list_model.appendRow(QStandardItem(self._ports[port_id].name)) + + if self._port_ids: + if selected_id not in self._ports: + selected_id = self._port_ids[0] + row = self._port_ids.index(selected_id) + index = self._list_model.index(row, 0) + self.ui.portList.setCurrentIndex(index) + else: + self._set_editor_enabled(False) + self.ui.removePort.setEnabled(False) + + def _selection_changed(self,_selected: QItemSelection,_deselected: QItemSelection) -> None: + port_id = self._selected_port_id() + 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]) + + def _selected_port_id(self) -> PortID | None: + index = self.ui.portList.currentIndex() + if not index.isValid() or index.row() >= len(self._port_ids): + return None + return self._port_ids[index.row()] + + def _load_port(self, port: Port) -> None: + self._loading = True + self.ui.nameEdit.setText(port.name) + self.ui.inputOrientation.setChecked(port.direction is SignalDirection.INPUT) + self.ui.outputOrientation.setChecked(port.direction is SignalDirection.OUTPUT) + self.ui.widthSize.setValue(port.matrix_size[0]) + self.ui.heightSize.setValue(port.matrix_size[1]) + self.ui.multiplicityCheckBox.setChecked(port.multiplicity) + self.ui.descriptionEdit.setPlainText(port.description or "") + + if isinstance(port, SignalPort): + self.ui.typeSignal.setChecked(True) + self.ui.signalTypeComboBox.setCurrentIndex( + self.ui.signalTypeComboBox.findData(port.value_type) + ) + else: + self.ui.typeBond.setChecked(True) + assert isinstance(port, BondPort) + if self.ui.domainComboBox.findText(port.domain) < 0: + self.ui.domainComboBox.addItem(port.domain) + self.ui.domainComboBox.setCurrentText(port.domain) + self.ui.causalityComboBox.setCurrentIndex(self.ui.causalityComboBox.findData(port.causality_preference)) + + self._loading = False + self._update_option_visibility() + + def _form_changed(self, *_args: object) -> None: + if self._loading: + return + + self._update_option_visibility() + port_id = self._selected_port_id() + if port_id is None: + return + + old_port = self._ports[port_id] + self._ports[port_id] = self._port_from_form(old_port) + self._list_model.item(self._port_ids.index(port_id)).setText( + self._ports[port_id].name + ) + self.ports_changed.emit() + + def _port_from_form(self, old_port: Port) -> Port: + common = { + "name": self.ui.nameEdit.text().strip(), + "direction": ( + SignalDirection.INPUT + if self.ui.inputOrientation.isChecked() + else SignalDirection.OUTPUT + ), + "multiplicity": self.ui.multiplicityCheckBox.isChecked(), + "matrix_size": [ + self.ui.widthSize.value(), + self.ui.heightSize.value(), + ], + "description": self.ui.descriptionEdit.toPlainText() or None, + } + if self.ui.typeSignal.isChecked(): + return SignalPort( + **common, + value_type=self.ui.signalTypeComboBox.currentData(), + quantity=( + old_port.quantity if isinstance(old_port, SignalPort) else None + ), + unit=old_port.unit if isinstance(old_port, SignalPort) else None, + ) + return BondPort( + **common, + domain=self.ui.domainComboBox.currentText(), + causality_preference=self.ui.causalityComboBox.currentData(), + ) + + def _add_port(self) -> None: + port_id = PortID() + number = len(self._ports) + 1 + port = SignalPort( + name=f"Port {number}", + direction=SignalDirection.INPUT, + ) + self._ports[port_id] = port + self._port_ids.append(port_id) + self._rebuild_list(port_id) + self.ports_changed.emit() + + def _remove_port(self) -> None: + port_id = self._selected_port_id() + if port_id is None: + return + row = self._port_ids.index(port_id) + del self._ports[port_id] + self._port_ids.remove(port_id) + selected = ( + self._port_ids[min(row, len(self._port_ids) - 1)] + if self._port_ids + else None + ) + self._rebuild_list(selected) + self.ports_changed.emit() + + def _update_option_visibility(self) -> None: + signal = self.ui.typeSignal.isChecked() + self._set_layout_visible(self.ui.signalOptions, signal) + self._set_layout_visible(self.ui.bondOptions, not signal) + + @staticmethod + def _set_layout_visible(layout: QLayout, visible: bool) -> None: + for index in range(layout.count()): + widget = layout.itemAt(index).widget() + if widget is not None: + widget.setVisible(visible) + + def _set_editor_enabled(self, enabled: bool) -> None: + for layout in ( + self.ui.basicForm, + self.ui.signalOptions, + self.ui.bondOptions, + self.ui.descriptionForm, + ): + for index in range(layout.count()): + widget = layout.itemAt(index).widget() + if widget is not None: + widget.setEnabled(enabled) diff --git a/tests/unit/test_port_editor.py b/tests/unit/test_port_editor.py new file mode 100644 index 0000000..e56f6fa --- /dev/null +++ b/tests/unit/test_port_editor.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from copy import deepcopy + +import pytest +from PySide6.QtWidgets import QApplication + +from bedit_core import models +from bedit_core.models import ( + BondPort, + Component, + Interface, + PortID, + SignalDirection, + SignalPort, + ValueType, +) +from bedit_gui.documents import Document +from bedit_gui.views.port_editor_widget import PortEditorWidget + + +@pytest.fixture(scope="module") +def qt_app() -> QApplication: + return QApplication.instance() or QApplication([]) + + +@pytest.mark.unit +def test_port_editor_uses_a_detached_draft(qt_app: QApplication) -> None: + port_id = PortID("input") + original = { + port_id: SignalPort( + name="Input", + direction=SignalDirection.INPUT, + ) + } + editor = PortEditorWidget() + editor.set_ports(original) + + editor.ui.nameEdit.setText("Changed") + editor.ui.nameEdit.textEdited.emit("Changed") + + assert original[port_id].name == "Input" + assert editor.ports()[port_id].name == "Changed" + + +@pytest.mark.unit +def test_port_type_controls_option_visibility(qt_app: QApplication) -> None: + editor = PortEditorWidget() + editor.set_ports( + { + PortID("port"): SignalPort( + name="Port", + direction=SignalDirection.INPUT, + value_type=ValueType.REAL, + ) + } + ) + + assert not editor.ui.signalTypeComboBox.isHidden() + assert editor.ui.domainComboBox.isHidden() + + editor.ui.typeBond.setChecked(True) + + assert editor.ui.signalTypeComboBox.isHidden() + assert not editor.ui.domainComboBox.isHidden() + + +@pytest.mark.unit +def test_bond_causality_is_loaded_and_changed(qt_app: QApplication) -> None: + port_id = PortID("bond") + editor = PortEditorWidget() + editor.set_ports({port_id: BondPort("Bond", SignalDirection.INPUT, causality_preference=models.PortCausality.PREFERRED_FLOW_OUT)}) + + assert editor.ui.causalityComboBox.currentData() is models.PortCausality.PREFERRED_FLOW_OUT + + index = editor.ui.causalityComboBox.findData(models.PortCausality.FIXED_EFFORT_OUT) + editor.ui.causalityComboBox.setCurrentIndex(index) + + assert editor.ports()[port_id].causality_preference is models.PortCausality.FIXED_EFFORT_OUT + + +@pytest.mark.unit +def test_port_changes_are_one_undoable_operation( + qt_app: QApplication, + minimal_document, +) -> None: + component: Component = next(iter(minimal_document.root.values())) + removed_id = PortID("removed") + changed_id = PortID("changed") + added_id = PortID("added") + component.interface = Interface( + { + removed_id: SignalPort("Remove", SignalDirection.INPUT), + changed_id: SignalPort("Before", SignalDirection.INPUT), + } + ) + original = deepcopy(component.interface.ports) + updated = { + changed_id: BondPort( + "After", + SignalDirection.OUTPUT, + domain="electrical", + ), + added_id: SignalPort("Added", SignalDirection.INPUT), + } + document = Document(qt_app) + + document.update_component_ports(component, updated) + assert component.interface.ports == updated + + document.undo_stack.undo() + assert component.interface.ports == original + + document.undo_stack.redo() + assert component.interface.ports == updated diff --git a/untitled.bedit.json b/untitled.bedit.json index 5f313f1..618a859 100644 --- a/untitled.bedit.json +++ b/untitled.bedit.json @@ -125,6 +125,20 @@ "description": "", "domain": "power", "causality_preference": "indifferent" + }, + "d1f5aab2-7bff-4b1e-8697-98fb6c3d8f02": { + "port_type": "signal", + "name": "power", + "direction": "output", + "multiplicity": false, + "matrix_size": [ + 1, + 1 + ], + "description": "Dissipated power", + "value_type": "real", + "quantity": null, + "unit": null } } }, @@ -147,7 +161,8 @@ "declarations": [], "initial_equations": [], "equations": [ - "p.e = r*p.f;" + "p.e = r*p.f;", + "power = p.e*p.f;" ] } },