Fixed some stuff

This commit is contained in:
2026-08-17 12:55:42 +02:00
parent cb9e03a6bf
commit 4591e6b7b0
13 changed files with 397 additions and 92 deletions

View File

@@ -83,6 +83,7 @@ class _CausalityEngine():
def clear_causalities(self) -> None:
for bond in self._network.bonds:
bond.connection.causality = BondCausality.NONE
bond.connection.undesired = False
def propagate_from_port(self, port: NetworkPort, causality: BondCausality) -> None:
attached_connections = self._network.bonds_for(port)
@@ -106,7 +107,7 @@ class _CausalityEngine():
def propagate_to_neighbor(self, component: Component, connection: NetworkBond) -> None:
# Get neighor
is_source = (component == connection.source.component)
neighbor = connection.target if is_source else connection.target
neighbor = connection.target if is_source else connection.source
# Direct the evaluation based on what type of port it is
if neighbor.port.causality_preference in [PortCausality.SINGLE_EFFORT_IN, PortCausality.SINGLE_FLOW_IN]:
self.evaluate_junction_constraints(neighbor)

View File

@@ -0,0 +1,44 @@
from PySide6.QtGui import QUndoCommand
from bedit_core.models import BondCausality, BondConnection, Component, ConnectionID, GraphImplementation
CausalityState = dict[ConnectionID, tuple[BondCausality, bool]]
def causality_state(component: Component) -> CausalityState:
state = {}
if not isinstance(component.implementation, GraphImplementation):
return state
for connection_id, connection in component.implementation.graph.connections.items():
if isinstance(connection, BondConnection):
state[connection_id] = (connection.causality, connection.undesired)
for child in component.implementation.graph.components.values():
state.update(causality_state(child))
return state
class ChangeCausalityCommand(QUndoCommand):
def __init__(self, document: object, component: Component, inferred_component: Component) -> None:
super().__init__("Infer causality")
self.document = document
self.component = component
self.old_state = causality_state(component)
self.new_state = causality_state(inferred_component)
def redo(self) -> None:
self._apply(self.component, self.new_state)
self.document.model_changed.emit(self.document.model)
def undo(self) -> None:
self._apply(self.component, self.old_state)
self.document.model_changed.emit(self.document.model)
@classmethod
def _apply(cls, component: Component, state: CausalityState) -> None:
if not isinstance(component.implementation, GraphImplementation):
return
for connection_id, connection in component.implementation.graph.connections.items():
if isinstance(connection, BondConnection) and connection_id in state:
connection.causality, connection.undesired = state[connection_id]
for child in component.implementation.graph.components.values():
cls._apply(child, state)

View File

@@ -0,0 +1,54 @@
from copy import deepcopy
from PySide6.QtGui import QUndoCommand
from bedit_core.models import Component, Connection, ConnectionID, GraphImplementation
class AddGraphConnectionCommand(QUndoCommand):
def __init__(self, document: object, graph_component: Component, connection: Connection) -> None:
super().__init__("Add connection")
if not isinstance(graph_component.implementation, GraphImplementation):
raise TypeError("component must have a graph implementation")
self.document = document
self.graph = graph_component.implementation.graph
self.connection_id = ConnectionID()
self.connection = connection
def redo(self) -> None:
self.graph.connections[self.connection_id] = self.connection
self.document.model_changed.emit(self.document.model)
def undo(self) -> None:
self.graph.connections.pop(self.connection_id, None)
self.document.model_changed.emit(self.document.model)
class DeleteGraphConnectionCommand(QUndoCommand):
def __init__(self, document: object, graph_component: Component, connection_id: ConnectionID) -> None:
super().__init__("Delete connection")
if not isinstance(graph_component.implementation, GraphImplementation):
raise TypeError("component must have a graph implementation")
self.document = document
self.graph_id = document.component_id(graph_component)
self.connections = graph_component.implementation.graph.connections
self.connection_id = connection_id
self.connection = self.connections[connection_id]
self.index = list(self.connections).index(connection_id)
database = document._graph_database(False)
graph = database.graphs.get(self.graph_id) if database is not None else None
self.visual_connection = deepcopy(graph.connections.get(connection_id)) if graph is not None else None
def redo(self) -> None:
self.connections.pop(self.connection_id, None)
self.document._set_graph_connection_points(self.graph_id, self.connection_id, None)
self.document.model_changed.emit(self.document.model)
def undo(self) -> None:
items = list(self.connections.items())
items.insert(self.index, (self.connection_id, self.connection))
self.connections.clear()
self.connections.update(items)
if self.visual_connection is not None:
self.document._set_graph_connection_points(self.graph_id, self.connection_id, self.visual_connection.points)
self.document.model_changed.emit(self.document.model)

View File

@@ -196,7 +196,7 @@ class GraphEditorClipboardHandler(ClipboardHandler):
return self._target() is not None and self.clipboard.has_format(ClipboardService.COMPONENTS_MIME)
def can_delete(self) -> bool:
return self.can_copy()
return bool(self._selected_components() or self.editor.selected_connection_ids())
def copy(self) -> None:
components = self._selected_components()
@@ -225,7 +225,13 @@ class GraphEditorClipboardHandler(ClipboardHandler):
self.document.paste_graph_components(graph_component, components, icons, positions)
def delete(self) -> None:
self.document.delete_components(self._selected_components())
components = self._selected_components()
connection_ids = self.editor.selected_connection_ids()
if components:
self.document.delete_components(components)
graph_component = self.editor.component()
if graph_component is not None and connection_ids:
self.document.delete_graph_connections(graph_component, connection_ids)
def _write_components(self, components: list[Component]) -> None:
payload = export_components(self.document, components)

View File

@@ -2,7 +2,7 @@ from collections.abc import Callable
from functools import partial
from typing import Protocol
from PySide6.QtCore import QEvent, QObject, QPoint, QSize, Qt
from PySide6.QtCore import QEvent, QItemSelectionModel, QObject, QPoint, QSize, Qt
from PySide6.QtGui import QMouseEvent
from PySide6.QtWidgets import QAbstractItemView, QDialog, QHeaderView, QMenu
@@ -69,7 +69,10 @@ class DocumentTreeController(QObject):
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_context_menu_requested.connect(self._show_graph_component_context_menu)
window.graph_editor.component_open_requested.connect(self._open_graph_component)
window.graph_editor.connection_points_change_requested.connect(self.document.change_graph_connection_points)
window.graph_editor.connection_add_requested.connect(self.document.add_graph_connection)
window.graph_editor.connections_delete_requested.connect(self.document.delete_graph_connections)
# Add deselection with esc to this widget
window.ui.actionEscape.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
@@ -176,6 +179,12 @@ class DocumentTreeController(QObject):
if component is not None:
self._show_component_context_menu(component, global_position)
def _open_graph_component(self, component_id: ComponentID) -> None:
index = self.model.component_index(component_id)
if index.isValid():
self.window.ui.documentTree.selectionModel().setCurrentIndex(index, QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows)
self.window.ui.documentTree.scrollTo(index)
def _show_component_context_menu(self, component: Component, global_position: QPoint) -> None:
menu = QMenu(self.window.ui.documentTree)
edit_interface = menu.addAction("Edit Interface")

View File

@@ -62,6 +62,7 @@ class SimulationController(QObject):
self.window.statusBar().showMessage(f"Compiling {component_path}")
logger.info("Compiling model: %s", component_path)
try:
self.document.infer_causality(component)
build = self.compiler(component, build_directory)
except (OSError, RuntimeError, ValueError) as exc:
logger.exception("Could not compile model %s", component_path)

View File

@@ -6,12 +6,15 @@ from pathlib import Path
from PySide6.QtCore import QObject, Signal
from PySide6.QtGui import QUndoStack
from bedit_core.models import ID, Component, ComponentID, ConnectionID, GraphImplementation, Port, PortID, Parameter, ParameterID
from bedit_core.models import ID, Component, ComponentID, Connection, ConnectionID, GraphImplementation, Port, PortID, Parameter, ParameterID
from bedit_core.models import Document as CoreDocument
from bedit_core.bondgraph import causality_inference
from bedit_gui.commands.causality_command import ChangeCausalityCommand, causality_state
from bedit_gui.commands.change_icon_command import ChangeIconCommand
from bedit_gui.commands.equation_text_command import ChangeEquationTextCommand
from bedit_gui.commands.graph_position_command import MoveGraphComponentCommand
from bedit_gui.commands.graph_connection_points_command import ChangeGraphConnectionPointsCommand
from bedit_gui.commands.graph_connection_command import AddGraphConnectionCommand, DeleteGraphConnectionCommand
from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand
from bedit_gui.commands.param_commands import AddParamCommand, ChangeParamCommand, RemoveParamCommand
from bedit_gui.commands.rename_component_command import RenameComponentCommand
@@ -102,6 +105,12 @@ class Document(QObject):
def rename_component(self, component: Component, name: str) -> None:
self.undo_stack.push(RenameComponentCommand(self, component, name))
def infer_causality(self, component: Component) -> None:
inferred_component = deepcopy(component)
causality_inference(inferred_component)
if causality_state(component) != causality_state(inferred_component):
self.undo_stack.push(ChangeCausalityCommand(self, component, inferred_component))
def component_id(self, component: Component) -> ComponentID:
def find(components: dict[ComponentID, Component]) -> ComponentID | None:
for component_id, candidate in components.items():
@@ -169,6 +178,20 @@ class Document(QObject):
graph_id = self.component_id(graph_component)
self.undo_stack.push(ChangeGraphConnectionPointsCommand(self, graph_id, connection_id, points, text))
def add_graph_connection(self, graph_component: Component, connection: Connection) -> None:
self.undo_stack.push(AddGraphConnectionCommand(self, graph_component, connection))
def delete_graph_connections(self, graph_component: Component, connection_ids: list[ConnectionID]) -> None:
if not isinstance(graph_component.implementation, GraphImplementation):
return
connection_ids = [connection_id for connection_id in connection_ids if connection_id in graph_component.implementation.graph.connections]
if not connection_ids:
return
self.undo_stack.beginMacro("Delete connections")
for connection_id in connection_ids:
self.undo_stack.push(DeleteGraphConnectionCommand(self, graph_component, connection_id))
self.undo_stack.endMacro()
def _set_graph_connection_points(self, graph_id: ComponentID, connection_id: ConnectionID, points: list[tuple[int, int]] | None) -> None:
if points is None:
database = self._graph_database(False)

View File

@@ -72,6 +72,9 @@
</property>
</action>
<action name="actionMouseMode">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/edit-select.png</normaloff>:/icons/icons/edit-select.png</iconset>
@@ -84,6 +87,9 @@
</property>
</action>
<action name="actionConnectionMode">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/network-connect.png</normaloff>:/icons/icons/network-connect.png</iconset>

View File

@@ -32,11 +32,13 @@ class Ui_graphEditorWidget(object):
self.actionZoomToFit.setIcon(icon)
self.actionMouseMode = QAction(graphEditorWidget)
self.actionMouseMode.setObjectName(u"actionMouseMode")
self.actionMouseMode.setCheckable(True)
icon1 = QIcon()
icon1.addFile(u":/icons/icons/edit-select.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionMouseMode.setIcon(icon1)
self.actionConnectionMode = QAction(graphEditorWidget)
self.actionConnectionMode.setObjectName(u"actionConnectionMode")
self.actionConnectionMode.setCheckable(True)
icon2 = QIcon()
icon2.addFile(u":/icons/icons/network-connect.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionConnectionMode.setIcon(icon2)

View File

@@ -1,13 +1,14 @@
from __future__ import annotations
from enum import Enum
from itertools import pairwise
from math import hypot
from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, QSize, QTimer, Qt, Signal
from PySide6.QtGui import QBrush, QColor, QCursor, QPainter, QPainterPath, QPen, QWheelEvent
from PySide6.QtGui import QActionGroup, QBrush, QColor, QCursor, QKeySequence, QMouseEvent, QPainter, QPainterPath, QPen, QShortcut, QWheelEvent
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsView, QMenu, QWidget
from bedit_core.models import BondCausality, BondConnection, Component, ComponentID, ConnectionID, GraphImplementation, SignalConnection
from bedit_core.models import BondCausality, BondConnection, BondPort, Component, ComponentID, Connection, ConnectionID, GraphImplementation, Port, PortID, SignalConnection, SignalDirection, SignalPort
from bedit_gui.models import Graph, GraphConnection, Icon
from bedit_gui.ui.generated.ui_graph_editor_widget import Ui_graphEditorWidget
from bedit_gui.utils.icon import get_pixmap_bounding_box, render_icon
@@ -28,6 +29,11 @@ ARROW_HALF_WIDTH = 16.0
CAUSALITY_TICK_HALF_LENGTH = 16.0
class GraphEditorMode(Enum):
NORMAL = "normal"
CONNECTION = "connection"
class GraphGraphicsScene(QGraphicsScene):
"""Graph canvas with a lightweight dotted-line grid."""
@@ -59,15 +65,23 @@ class GraphConnectionItem(QGraphicsPathItem):
self.setPath(self._connection_path(points, half_arrow, tick_at_source))
pen = QPen(QColor("#202020"), CONNECTION_WIDTH)
self.setPen(pen)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.setZValue(-1)
def contextMenuEvent(self, event) -> None:
if self.editor is None or self.connection_id is None:
return
if not self.isSelected():
self.scene().clearSelection()
self.setSelected(True)
menu = QMenu(self.editor)
add_point = menu.addAction("Add Point")
if menu.exec(event.screenPos()) is add_point:
delete_connection = menu.addAction("Delete Connection")
selected = menu.exec(event.screenPos())
if selected is add_point:
self.editor.add_connection_point(self.connection_id, event.scenePos())
elif selected is delete_connection:
self.editor.delete_selected_connections()
event.accept()
@staticmethod
@@ -123,7 +137,7 @@ class GraphComponentItem(QGraphicsPixmapItem):
self._drag_start = QPointF()
self._dragging = False
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, editor.mode is GraphEditorMode.NORMAL)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object:
@@ -136,11 +150,18 @@ class GraphComponentItem(QGraphicsPixmapItem):
return result
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if event.button() == Qt.MouseButton.LeftButton and self.editor.mode is GraphEditorMode.CONNECTION:
self.editor.choose_connection_component(self.component_id, event.screenPos())
event.accept()
return
self._drag_start = QPointF(self.pos())
self._dragging = True
super().mousePressEvent(event)
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if self.editor.mode is GraphEditorMode.CONNECTION:
event.accept()
return
super().mouseReleaseEvent(event)
self._dragging = False
position = (round(self.pos().x()), round(self.pos().y()))
@@ -148,6 +169,13 @@ class GraphComponentItem(QGraphicsPixmapItem):
if self.pos() != self._drag_start:
self.editor.finish_component_move(self.component_id, position)
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None:
if event.button() == Qt.MouseButton.LeftButton:
self.editor.open_component(self.component_id)
event.accept()
return
super().mouseDoubleClickEvent(event)
def contextMenuEvent(self, event) -> None:
if not self.isSelected():
self.scene().clearSelection()
@@ -205,7 +233,10 @@ class GraphConnectionPointItem(QGraphicsEllipseItem):
class GraphEditorWidget(QWidget):
component_move_requested = Signal(object, object, object)
component_context_menu_requested = Signal(object, object)
component_open_requested = Signal(object)
connection_points_change_requested = Signal(object, object, object, str)
connection_add_requested = Signal(object, object)
connections_delete_requested = Signal(object, object)
def __init__(self, parent: QWidget | None = None, snap_to_grid_size: int = 4) -> None:
super().__init__(parent)
@@ -214,6 +245,10 @@ class GraphEditorWidget(QWidget):
self.ui.setupUi(self)
self._component: Component | None = None
self._graph = Graph()
self._icons: dict[ComponentID, Icon] = {}
self._mode = GraphEditorMode.NORMAL
self._connection_start: ComponentID | None = None
self._connection_preview: QGraphicsPathItem | None = None
self._component_items: dict[ComponentID, GraphComponentItem] = {}
self._component_bounds: dict[ComponentID, QRectF] = {}
self._connection_items: dict[ConnectionID, GraphConnectionItem] = {}
@@ -224,7 +259,18 @@ class GraphEditorWidget(QWidget):
self.ui.graphicsView.setScene(self.scene)
self.ui.graphicsView.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
self.ui.graphicsView.viewport().installEventFilter(self)
self._mode_actions = QActionGroup(self)
self._mode_actions.setExclusive(True)
self._mode_actions.addAction(self.ui.actionMouseMode)
self._mode_actions.addAction(self.ui.actionConnectionMode)
self.ui.actionMouseMode.triggered.connect(lambda: self.set_mode(GraphEditorMode.NORMAL))
self.ui.actionConnectionMode.triggered.connect(lambda: self.set_mode(GraphEditorMode.CONNECTION))
self.ui.actionMouseMode.setChecked(True)
self._mode_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Space), self)
self._mode_shortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
self._mode_shortcut.activated.connect(self.toggle_mode)
self.ui.actionZoomToFit.triggered.connect(self.zoom_to_fit)
self.ui.graphicsView.viewport().setMouseTracking(True)
self.ui.graphicsView.centerOn(0, 0)
@property
@@ -236,12 +282,32 @@ class GraphEditorWidget(QWidget):
raise ValueError("snap-to-grid size must be positive")
self._snap_to_grid_size = size
@property
def mode(self) -> GraphEditorMode:
return self._mode
def set_mode(self, mode: GraphEditorMode) -> None:
if mode is self._mode:
return
self._mode = mode
self.ui.actionMouseMode.setChecked(mode is GraphEditorMode.NORMAL)
self.ui.actionConnectionMode.setChecked(mode is GraphEditorMode.CONNECTION)
self._clear_connection_start()
component = self._component
if component is not None:
self.set_component(component, self._graph, self._icons)
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:
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
self._clear_connection_start()
self._component = component
self._graph = graph or Graph()
self._icons = icons or {}
self._component_items = {}
self._component_bounds = {}
self._connection_items = {}
@@ -251,11 +317,11 @@ class GraphEditorWidget(QWidget):
return
graph = self._graph
icons = icons or {}
icons = self._icons
positions = {component_id: graph.component_positions.get(component_id, (index * FALLBACK_COMPONENT_SPACING, 0)) for index, component_id in enumerate(component.implementation.graph.components)}
for component_id, child in component.implementation.graph.components.items():
icon = icons.get(component_id, Icon())
pixmap = render_icon(icon, child.interface.ports, COMPONENT_ICON_SIZE).pixmap(COMPONENT_ICON_SIZE)
pixmap = render_icon(icon, child.interface.ports, COMPONENT_ICON_SIZE, render_ports=self._mode is GraphEditorMode.CONNECTION).pixmap(COMPONENT_ICON_SIZE)
item = GraphComponentItem(component_id, pixmap, self)
item.setOffset(-pixmap.width() / 2, -pixmap.height() / 2)
item.setPos(*positions[component_id])
@@ -290,7 +356,9 @@ class GraphEditorWidget(QWidget):
return
port_owners = {port_id: component_id for component_id, child in component.implementation.graph.components.items() for port_id in child.interface.ports}
for connection_id, item in self._connection_items.items():
connection = component.implementation.graph.connections[connection_id]
connection = component.implementation.graph.connections.get(connection_id)
if connection is None:
continue
source_component = port_owners.get(connection.source)
target_component = port_owners.get(connection.target)
if source_component is None or target_component is None:
@@ -327,10 +395,14 @@ class GraphEditorWidget(QWidget):
def set_connection_points(self, connection_id: ConnectionID, points: list[tuple[int, int]] | None) -> None:
if points is None:
self._graph.connections.pop(connection_id, None)
interior_points = []
else:
self._graph.connections[connection_id] = GraphConnection(points=list(points))
interior_points = points[1:-1]
for item in self._connection_point_items.pop(connection_id, []):
self.scene.removeItem(item)
connection_item = self._connection_items.pop(connection_id, None)
if connection_item is not None:
self.scene.removeItem(connection_item)
return
self._graph.connections[connection_id] = GraphConnection(points=list(points))
interior_points = points[1:-1]
items = self._connection_point_items.get(connection_id, [])
if len(items) == len(interior_points):
for item, position in zip(items, interior_points):
@@ -392,8 +464,11 @@ class GraphEditorWidget(QWidget):
if item is None:
return
if position is None:
self._graph.component_positions.pop(component_id, None)
index = list(self._component_items).index(component_id)
position = (index * FALLBACK_COMPONENT_SPACING, 0)
else:
self._graph.component_positions[component_id] = position
item.setPos(*position)
self.refresh_connections()
@@ -445,6 +520,112 @@ class GraphEditorWidget(QWidget):
def selected_component_ids(self) -> list[ComponentID]:
return [item.component_id for item in self.scene.selectedItems() if isinstance(item, GraphComponentItem)]
def selected_connection_ids(self) -> list[ConnectionID]:
return [item.connection_id for item in self.scene.selectedItems() if isinstance(item, GraphConnectionItem) and item.connection_id is not None]
def delete_selected_connections(self) -> None:
if self._component is not None:
self.connections_delete_requested.emit(self._component, self.selected_connection_ids())
def open_component(self, component_id: ComponentID) -> None:
self._clear_connection_start()
self.component_open_requested.emit(component_id)
def choose_connection_component(self, component_id: ComponentID, screen_position) -> None:
if self._component is None or self._mode is not GraphEditorMode.CONNECTION:
return
if self._connection_start is None:
self.scene.clearSelection()
self._component_items[component_id].setSelected(True)
self._connection_start = component_id
self._connection_preview = QGraphicsPathItem()
self._connection_preview.setPen(QPen(QColor("#606060"), CONNECTION_WIDTH, Qt.PenStyle.DashLine))
self._connection_preview.setZValue(-0.5)
self.scene.addItem(self._connection_preview)
return
start = self._connection_start
self._clear_connection_start()
self.scene.clearSelection()
if start == component_id:
return
options = self._connection_options(start, component_id)
if len(options) == 1:
self.connection_add_requested.emit(self._component, options[0][1])
return
if not options:
return
menu = QMenu(self)
actions = []
for label, connection, _preferred in options:
action = menu.addAction(label)
actions.append((action, connection))
menu.setActiveAction(actions[0][0])
selected = menu.exec(screen_position)
for action, connection in actions:
if selected is action:
self.connection_add_requested.emit(self._component, connection)
break
def _update_connection_preview(self, mouse_position: QPointF) -> None:
if self._connection_start is None or self._connection_preview is None:
return
source = self._item_position(self._connection_start)
if mouse_position == QPointF(*source):
self._connection_preview.setPath(QPainterPath(mouse_position))
return
bounds = self._component_bounds[self._connection_start].translated(*source)
start = self._bounding_box_edge(bounds, source, (mouse_position.x(), mouse_position.y()))
path = QPainterPath(QPointF(*start))
path.lineTo(mouse_position)
self._connection_preview.setPath(path)
def _clear_connection_start(self) -> None:
self._connection_start = None
if self._connection_preview is not None and self._connection_preview.scene() is self.scene:
self.scene.removeItem(self._connection_preview)
self._connection_preview = None
def _connection_options(self, first_id: ComponentID, second_id: ComponentID) -> list[tuple[str, Connection, bool]]:
if self._component is None:
return []
components = self._component.implementation.graph.components
first = components[first_id]
second = components[second_id]
used_ports = {port_id for connection in self._component.implementation.graph.connections.values() for port_id in (connection.source, connection.target)}
options = []
for first_port_id, first_port in first.interface.ports.items():
for second_port_id, second_port in second.interface.ports.items():
if not self._compatible_ports(first_port, second_port) or not self._port_available(first_port_id, first_port, used_ports) or not self._port_available(second_port_id, second_port, used_ports):
continue
preferred = first_port.direction is SignalDirection.OUTPUT and second_port.direction is SignalDirection.INPUT
reverse_preferred = second_port.direction is SignalDirection.OUTPUT and first_port.direction is SignalDirection.INPUT
if reverse_preferred:
source_id, source_port, source_name = second_port_id, second_port, second.name
target_id, target_port, target_name = first_port_id, first_port, first.name
else:
source_id, source_port, source_name = first_port_id, first_port, first.name
target_id, target_port, target_name = second_port_id, second_port, second.name
connection_type = SignalConnection if isinstance(source_port, SignalPort) else BondConnection
label = f"{source_name}.{source_port.name}{target_name}.{target_port.name}"
options.append((label, connection_type(source=source_id, target=target_id), preferred or reverse_preferred))
return sorted(options, key=lambda option: not option[2])
@staticmethod
def _compatible_ports(first: Port, second: Port) -> bool:
if isinstance(first, SignalPort) and isinstance(second, SignalPort):
return first.direction is not second.direction
if isinstance(first, BondPort) and isinstance(second, BondPort):
return not first.domain or not second.domain or first.domain == second.domain
return False
@staticmethod
def _port_available(port_id: PortID, port: Port, used_ports: set[PortID]) -> bool:
if isinstance(port, SignalPort):
return port.direction is SignalDirection.OUTPUT or port_id not in used_ports
if isinstance(port, BondPort):
return port.multiplicity or port_id not in used_ports
return False
def paste_position(self) -> tuple[int, int]:
viewport = self.ui.graphicsView.viewport()
viewport_position = viewport.mapFromGlobal(QCursor.pos())
@@ -469,6 +650,9 @@ class GraphEditorWidget(QWidget):
self.ui.graphicsView.centerOn(bounds.center())
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
if watched is self.ui.graphicsView.viewport() and event.type() == QEvent.Type.MouseMove:
assert isinstance(event, QMouseEvent)
self._update_connection_preview(self.ui.graphicsView.mapToScene(event.position().toPoint()))
if watched is self.ui.graphicsView.viewport() and event.type() == QEvent.Type.Wheel:
assert isinstance(event, QWheelEvent)
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:

View File

@@ -131,6 +131,12 @@ class DocumentTreeModel(QAbstractItemModel):
node = index.internalPointer()
return node.value if isinstance(node, DocumentTreeNode) else None
def component_index(self, component_id: ComponentID) -> QModelIndex:
node = self._component_nodes.get(component_id)
if node is None or node.parent is None:
return QModelIndex()
return self.createIndex(node.parent.children.index(node), 0, node)
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
flags = super().flags(index)

View File

@@ -374,6 +374,7 @@ class Simulation:
process = self._run_model_commands(
modelica,
[
f"checkModel({model_name})",
f'buildModel({model_name}, outputFormat="csv")',
"getErrorString()",
],

View File

@@ -360,20 +360,6 @@
"connection_type": "bond",
"source": "497b1f74-1186-471f-976a-36b07a451caf",
"target": "4a3f69f2-b305-4305-92e3-3998634ff226",
"causality": "flow_out",
"undesired": false
},
"8aaa96b3-45ed-4674-8f7a-06b28178a5c8": {
"connection_type": "bond",
"source": "4306701a-6b1b-4d19-b8ff-45dbfa04f2d3",
"target": "c87881f1-23b4-4a69-b586-8e3c7bf6e21e",
"causality": "flow_out",
"undesired": false
},
"ff606583-d31f-4638-93f7-926163db2665": {
"connection_type": "bond",
"source": "4306701a-6b1b-4d19-b8ff-45dbfa04f2d3",
"target": "0ac7d4ea-77f7-4c0d-8e37-406ef66e4740",
"causality": "effort_out",
"undesired": false
},
@@ -384,10 +370,24 @@
"causality": "effort_out",
"undesired": false
},
"5af42b61-2317-48a8-b32b-0e2dfd88368e": {
"cfff2ad4-a4ec-4f37-aa36-2c37b01422f0": {
"connection_type": "bond",
"source": "4306701a-6b1b-4d19-b8ff-45dbfa04f2d3",
"target": "497b1f74-1186-471f-976a-36b07a451caf",
"causality": "effort_out",
"undesired": false
},
"743e35a4-e736-407d-bf34-b4afbcde5a0b": {
"connection_type": "bond",
"source": "4306701a-6b1b-4d19-b8ff-45dbfa04f2d3",
"target": "0ac7d4ea-77f7-4c0d-8e37-406ef66e4740",
"causality": "flow_out",
"undesired": false
},
"68727b23-888b-45b4-a0f4-801ac7b39601": {
"connection_type": "bond",
"source": "4306701a-6b1b-4d19-b8ff-45dbfa04f2d3",
"target": "c87881f1-23b4-4a69-b586-8e3c7bf6e21e",
"causality": "flow_out",
"undesired": false
}
@@ -458,31 +458,6 @@
]
}
},
"77701f68-b14c-4b98-8929-c5fba0261962": {
"shapes": {
"2a5bf670-c821-442b-9d46-1b07faa70722": {
"layer": 0,
"type": "text",
"pos": [
-32,
-32
],
"width": 64.0,
"height": 64.0,
"color": "#000000ff",
"bold": true,
"italic": false,
"size": 64.0,
"text": "R"
}
},
"port_positions": {
"4a3f69f2-b305-4305-92e3-3998634ff226": [
-8,
-8
]
}
},
"033b930e-bf79-403a-8b0b-a159f3c81ce9": {
"shapes": {
"2a5bf670-c821-442b-9d46-1b07faa70722": {
@@ -586,6 +561,31 @@
-8
]
}
},
"77701f68-b14c-4b98-8929-c5fba0261962": {
"shapes": {
"2a5bf670-c821-442b-9d46-1b07faa70722": {
"layer": 0,
"type": "text",
"pos": [
-32,
-32
],
"width": 64.0,
"height": 64.0,
"color": "#000000ff",
"bold": true,
"italic": false,
"size": 64.0,
"text": "R"
}
},
"port_positions": {
"4a3f69f2-b305-4305-92e3-3998634ff226": [
-8,
-8
]
}
}
}
},
@@ -622,6 +622,10 @@
"033b930e-bf79-403a-8b0b-a159f3c81ce9": [
-192,
128
],
"1455acad-931b-4caa-be71-aff4283507c6": [
64,
384
]
},
"connections": {
@@ -649,30 +653,6 @@
]
]
},
"8aaa96b3-45ed-4674-8f7a-06b28178a5c8": {
"points": [
[
-128,
-128
],
[
128,
-256
]
]
},
"ff606583-d31f-4638-93f7-926163db2665": {
"points": [
[
-128,
-128
],
[
128,
-64
]
]
},
"1f52dc5a-1847-4982-aabc-2538cf99a86f": {
"points": [
[
@@ -684,18 +664,6 @@
224
]
]
},
"5af42b61-2317-48a8-b32b-0e2dfd88368e": {
"points": [
[
-128,
-128
],
[
128,
160
]
]
}
}
}