Fixed some stuff
This commit is contained in:
@@ -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)
|
||||
|
||||
44
src/bedit_gui/commands/causality_command.py
Normal file
44
src/bedit_gui/commands/causality_command.py
Normal 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)
|
||||
54
src/bedit_gui/commands/graph_connection_command.py
Normal file
54
src/bedit_gui/commands/graph_connection_command.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,8 +395,12 @@ 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:
|
||||
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, [])
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -374,6 +374,7 @@ class Simulation:
|
||||
process = self._run_model_commands(
|
||||
modelica,
|
||||
[
|
||||
f"checkModel({model_name})",
|
||||
f'buildModel({model_name}, outputFormat="csv")',
|
||||
"getErrorString()",
|
||||
],
|
||||
|
||||
@@ -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
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user