Connection points
This commit is contained in:
24
src/bedit_gui/commands/graph_connection_points_command.py
Normal file
24
src/bedit_gui/commands/graph_connection_points_command.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from PySide6.QtGui import QUndoCommand
|
||||||
|
|
||||||
|
from bedit_core.models import ComponentID, ConnectionID
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeGraphConnectionPointsCommand(QUndoCommand):
|
||||||
|
def __init__(self, document: object, graph_id: ComponentID, connection_id: ConnectionID, points: list[tuple[int, int]], text: str) -> None:
|
||||||
|
super().__init__(text)
|
||||||
|
self.document = document
|
||||||
|
self.graph_id = graph_id
|
||||||
|
self.connection_id = connection_id
|
||||||
|
database = document._graph_database(False)
|
||||||
|
graph = database.graphs.get(graph_id) if database is not None else None
|
||||||
|
connection = graph.connections.get(connection_id) if graph is not None else None
|
||||||
|
self.old_points = deepcopy(connection.points) if connection is not None else None
|
||||||
|
self.new_points = deepcopy(points)
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.document._set_graph_connection_points(self.graph_id, self.connection_id, self.new_points)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.document._set_graph_connection_points(self.graph_id, self.connection_id, self.old_points)
|
||||||
@@ -6,7 +6,7 @@ from PySide6.QtCore import QEvent, QObject, QPoint, QSize, Qt
|
|||||||
from PySide6.QtGui import QMouseEvent
|
from PySide6.QtGui import QMouseEvent
|
||||||
from PySide6.QtWidgets import QAbstractItemView, QDialog, QHeaderView, QMenu
|
from PySide6.QtWidgets import QAbstractItemView, QDialog, QHeaderView, QMenu
|
||||||
|
|
||||||
from bedit_core.models import Component, ComponentID, EquationImplementation, GraphImplementation, Port, PortID, Parameter, ParameterID
|
from bedit_core.models import Component, ComponentID, ConnectionID, EquationImplementation, GraphImplementation, Port, PortID, Parameter, ParameterID
|
||||||
from bedit_core.models import Document as CoreDocument
|
from bedit_core.models import Document as CoreDocument
|
||||||
from bedit_gui.documents import Document
|
from bedit_gui.documents import Document
|
||||||
from bedit_gui.models import Graph, Icon
|
from bedit_gui.models import Graph, Icon
|
||||||
@@ -63,11 +63,13 @@ class DocumentTreeController(QObject):
|
|||||||
document.model_changed.connect(self._on_document_changed)
|
document.model_changed.connect(self._on_document_changed)
|
||||||
document.icon_changed.connect(self._on_icon_changed)
|
document.icon_changed.connect(self._on_icon_changed)
|
||||||
document.graph_component_position_changed.connect(self._on_graph_component_position_changed)
|
document.graph_component_position_changed.connect(self._on_graph_component_position_changed)
|
||||||
|
document.graph_connection_points_changed.connect(self._on_graph_connection_points_changed)
|
||||||
document.equation_text_changed.connect(self._on_equation_text_changed)
|
document.equation_text_changed.connect(self._on_equation_text_changed)
|
||||||
self.model.rename_document_requested.connect(self.document.rename)
|
self.model.rename_document_requested.connect(self.document.rename)
|
||||||
self.model.rename_component_requested.connect(self.document.rename_component)
|
self.model.rename_component_requested.connect(self.document.rename_component)
|
||||||
window.graph_editor.component_move_requested.connect(self.document.move_graph_component)
|
window.graph_editor.component_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_context_menu_requested.connect(self._show_graph_component_context_menu)
|
||||||
|
window.graph_editor.connection_points_change_requested.connect(self.document.change_graph_connection_points)
|
||||||
|
|
||||||
# Add deselection with esc to this widget
|
# Add deselection with esc to this widget
|
||||||
window.ui.actionEscape.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
|
window.ui.actionEscape.setShortcutContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
|
||||||
@@ -150,6 +152,11 @@ class DocumentTreeController(QObject):
|
|||||||
if graph_component is not None and self.document.component_id(graph_component) == graph_id:
|
if graph_component is not None and self.document.component_id(graph_component) == graph_id:
|
||||||
self.window.graph_editor.set_component_position(component_id, position)
|
self.window.graph_editor.set_component_position(component_id, position)
|
||||||
|
|
||||||
|
def _on_graph_connection_points_changed(self, graph_id: ComponentID, connection_id: ConnectionID, points: list[tuple[int, int]] | None) -> None:
|
||||||
|
graph_component = self.window.graph_editor.component()
|
||||||
|
if graph_component is not None and self.document.component_id(graph_component) == graph_id:
|
||||||
|
self.window.graph_editor.set_connection_points(connection_id, points)
|
||||||
|
|
||||||
def _collect_components(self, components: dict[ComponentID, Component]) -> None:
|
def _collect_components(self, components: dict[ComponentID, Component]) -> None:
|
||||||
for component_id, component in components.items():
|
for component_id, component in components.items():
|
||||||
self._components[component_id] = component
|
self._components[component_id] = component
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ from pathlib import Path
|
|||||||
from PySide6.QtCore import QObject, Signal
|
from PySide6.QtCore import QObject, Signal
|
||||||
from PySide6.QtGui import QUndoStack
|
from PySide6.QtGui import QUndoStack
|
||||||
|
|
||||||
from bedit_core.models import ID, Component, ComponentID, GraphImplementation, Port, PortID, Parameter, ParameterID
|
from bedit_core.models import ID, Component, ComponentID, ConnectionID, GraphImplementation, Port, PortID, Parameter, ParameterID
|
||||||
from bedit_core.models import Document as CoreDocument
|
from bedit_core.models import Document as CoreDocument
|
||||||
from bedit_gui.commands.change_icon_command import ChangeIconCommand
|
from bedit_gui.commands.change_icon_command import ChangeIconCommand
|
||||||
from bedit_gui.commands.equation_text_command import ChangeEquationTextCommand
|
from bedit_gui.commands.equation_text_command import ChangeEquationTextCommand
|
||||||
from bedit_gui.commands.graph_position_command import MoveGraphComponentCommand
|
from bedit_gui.commands.graph_position_command import MoveGraphComponentCommand
|
||||||
|
from bedit_gui.commands.graph_connection_points_command import ChangeGraphConnectionPointsCommand
|
||||||
from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand
|
from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand
|
||||||
from bedit_gui.commands.param_commands import AddParamCommand, ChangeParamCommand, RemoveParamCommand
|
from bedit_gui.commands.param_commands import AddParamCommand, ChangeParamCommand, RemoveParamCommand
|
||||||
from bedit_gui.commands.rename_component_command import RenameComponentCommand
|
from bedit_gui.commands.rename_component_command import RenameComponentCommand
|
||||||
from bedit_gui.commands.rename_document_command import RenameDocumentCommand
|
from bedit_gui.commands.rename_document_command import RenameDocumentCommand
|
||||||
from bedit_gui.commands.simulation_database_command import ChangeSimulationDatabaseCommand
|
from bedit_gui.commands.simulation_database_command import ChangeSimulationDatabaseCommand
|
||||||
from bedit_gui.commands.component_command import AddEmptyEquationComponent, AddEmptyGraphComponent, DeleteComponent, PasteComponents
|
from bedit_gui.commands.component_command import AddEmptyEquationComponent, AddEmptyGraphComponent, DeleteComponent, PasteComponents
|
||||||
from bedit_gui.models import Graph, GraphDatabase, Icon, IconDatabase, Simulation, SimulationDatabase
|
from bedit_gui.models import Graph, GraphConnection, GraphDatabase, Icon, IconDatabase, Simulation, SimulationDatabase
|
||||||
from bedit_gui.services import document_files
|
from bedit_gui.services import document_files
|
||||||
|
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ class Document(QObject):
|
|||||||
equation_text_changed = Signal(object, str)
|
equation_text_changed = Signal(object, str)
|
||||||
simulation_database_changed = Signal(object)
|
simulation_database_changed = Signal(object)
|
||||||
graph_component_position_changed = Signal(object, object, object)
|
graph_component_position_changed = Signal(object, object, object)
|
||||||
|
graph_connection_points_changed = Signal(object, object, object)
|
||||||
|
|
||||||
def __init__(self, parent: QObject | None = None) -> None:
|
def __init__(self, parent: QObject | None = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -163,6 +165,22 @@ class Document(QObject):
|
|||||||
metadata["graph_database"] = database
|
metadata["graph_database"] = database
|
||||||
return database
|
return database
|
||||||
|
|
||||||
|
def change_graph_connection_points(self, graph_component: Component, connection_id: ConnectionID, points: list[tuple[int, int]], text: str) -> None:
|
||||||
|
graph_id = self.component_id(graph_component)
|
||||||
|
self.undo_stack.push(ChangeGraphConnectionPointsCommand(self, graph_id, connection_id, points, text))
|
||||||
|
|
||||||
|
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)
|
||||||
|
graph = database.graphs.get(graph_id) if database is not None else None
|
||||||
|
if graph is not None:
|
||||||
|
graph.connections.pop(connection_id, None)
|
||||||
|
else:
|
||||||
|
database = self._graph_database(True)
|
||||||
|
graph = database.graphs.setdefault(graph_id, Graph())
|
||||||
|
graph.connections[connection_id] = GraphConnection(points=list(points))
|
||||||
|
self.graph_connection_points_changed.emit(graph_id, connection_id, points)
|
||||||
|
|
||||||
def change_icon(self, component_id: ComponentID, icon: Icon) -> None:
|
def change_icon(self, component_id: ComponentID, icon: Icon) -> None:
|
||||||
self.undo_stack.push(ChangeIconCommand(self, component_id, icon))
|
self.undo_stack.push(ChangeIconCommand(self, component_id, icon))
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from itertools import pairwise
|
||||||
from math import hypot
|
from math import hypot
|
||||||
|
|
||||||
from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, QSize, QTimer, Qt, Signal
|
from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, QSize, QTimer, Qt, Signal
|
||||||
from PySide6.QtGui import QColor, QCursor, QPainter, QPainterPath, QPen, QWheelEvent
|
from PySide6.QtGui import QBrush, QColor, QCursor, QPainter, QPainterPath, QPen, QWheelEvent
|
||||||
from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsView, QWidget
|
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, Component, ComponentID, ConnectionID, GraphImplementation, SignalConnection
|
||||||
from bedit_gui.models import Graph, Icon
|
from bedit_gui.models import Graph, GraphConnection, Icon
|
||||||
from bedit_gui.ui.generated.ui_graph_editor_widget import Ui_graphEditorWidget
|
from bedit_gui.ui.generated.ui_graph_editor_widget import Ui_graphEditorWidget
|
||||||
from bedit_gui.utils.icon import get_pixmap_bounding_box, render_icon
|
from bedit_gui.utils.icon import get_pixmap_bounding_box, render_icon
|
||||||
|
|
||||||
@@ -51,13 +52,24 @@ class GraphGraphicsScene(QGraphicsScene):
|
|||||||
class GraphConnectionItem(QGraphicsPathItem):
|
class GraphConnectionItem(QGraphicsPathItem):
|
||||||
"""A routed connection with a full signal arrow or half bond arrow."""
|
"""A routed connection with a full signal arrow or half bond arrow."""
|
||||||
|
|
||||||
def __init__(self, points: list[tuple[float, float]], *, half_arrow: bool, tick_at_source: bool | None = None) -> None:
|
def __init__(self, points: list[tuple[float, float]], *, half_arrow: bool, tick_at_source: bool | None = None, connection_id: ConnectionID | None = None, editor: GraphEditorWidget | None = None) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.connection_id = connection_id
|
||||||
|
self.editor = editor
|
||||||
self.setPath(self._connection_path(points, half_arrow, tick_at_source))
|
self.setPath(self._connection_path(points, half_arrow, tick_at_source))
|
||||||
pen = QPen(QColor("#202020"), CONNECTION_WIDTH)
|
pen = QPen(QColor("#202020"), CONNECTION_WIDTH)
|
||||||
self.setPen(pen)
|
self.setPen(pen)
|
||||||
self.setZValue(-1)
|
self.setZValue(-1)
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event) -> None:
|
||||||
|
if self.editor is None or self.connection_id is None:
|
||||||
|
return
|
||||||
|
menu = QMenu(self.editor)
|
||||||
|
add_point = menu.addAction("Add Point")
|
||||||
|
if menu.exec(event.screenPos()) is add_point:
|
||||||
|
self.editor.add_connection_point(self.connection_id, event.scenePos())
|
||||||
|
event.accept()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _connection_path(points: list[tuple[float, float]], half_arrow: bool, tick_at_source: bool | None = None) -> QPainterPath:
|
def _connection_path(points: list[tuple[float, float]], half_arrow: bool, tick_at_source: bool | None = None) -> QPainterPath:
|
||||||
path = QPainterPath(QPointF(*points[0]))
|
path = QPainterPath(QPointF(*points[0]))
|
||||||
@@ -144,9 +156,56 @@ class GraphComponentItem(QGraphicsPixmapItem):
|
|||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
class GraphConnectionPointItem(QGraphicsEllipseItem):
|
||||||
|
def __init__(self, connection_id: ConnectionID, index: int, position: tuple[int, int], editor: GraphEditorWidget) -> None:
|
||||||
|
radius = CONNECTION_WIDTH
|
||||||
|
super().__init__(-radius, -radius, radius * 2, radius * 2)
|
||||||
|
self.connection_id = connection_id
|
||||||
|
self.index = index
|
||||||
|
self.editor = editor
|
||||||
|
self._drag_start = QPointF()
|
||||||
|
self._dragging = False
|
||||||
|
self.setPos(*position)
|
||||||
|
self.setPen(QPen(Qt.PenStyle.NoPen))
|
||||||
|
self.setBrush(QBrush(QColor("#202020")))
|
||||||
|
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)
|
||||||
|
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||||
|
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||||
|
|
||||||
|
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object:
|
||||||
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._dragging and isinstance(value, QPointF):
|
||||||
|
size = self.editor.snap_to_grid_size
|
||||||
|
value = QPointF(round(value.x() / size) * size, round(value.y() / size) * size)
|
||||||
|
result = super().itemChange(change, value)
|
||||||
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||||
|
self.editor.refresh_connections()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||||
|
self._drag_start = QPointF(self.pos())
|
||||||
|
self._dragging = True
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
self._dragging = False
|
||||||
|
position = (round(self.pos().x()), round(self.pos().y()))
|
||||||
|
self.setPos(*position)
|
||||||
|
if self.pos() != self._drag_start:
|
||||||
|
self.editor.finish_connection_point_move(self.connection_id)
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event) -> None:
|
||||||
|
menu = QMenu(self.editor)
|
||||||
|
delete_point = menu.addAction("Delete Point")
|
||||||
|
if menu.exec(event.screenPos()) is delete_point:
|
||||||
|
self.editor.delete_connection_point(self.connection_id, self.index)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
class GraphEditorWidget(QWidget):
|
class GraphEditorWidget(QWidget):
|
||||||
component_move_requested = Signal(object, object, object)
|
component_move_requested = Signal(object, object, object)
|
||||||
component_context_menu_requested = Signal(object, object)
|
component_context_menu_requested = Signal(object, object)
|
||||||
|
connection_points_change_requested = Signal(object, object, object, str)
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None, snap_to_grid_size: int = 4) -> None:
|
def __init__(self, parent: QWidget | None = None, snap_to_grid_size: int = 4) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -158,6 +217,7 @@ class GraphEditorWidget(QWidget):
|
|||||||
self._component_items: dict[ComponentID, GraphComponentItem] = {}
|
self._component_items: dict[ComponentID, GraphComponentItem] = {}
|
||||||
self._component_bounds: dict[ComponentID, QRectF] = {}
|
self._component_bounds: dict[ComponentID, QRectF] = {}
|
||||||
self._connection_items: dict[ConnectionID, GraphConnectionItem] = {}
|
self._connection_items: dict[ConnectionID, GraphConnectionItem] = {}
|
||||||
|
self._connection_point_items: dict[ConnectionID, list[GraphConnectionPointItem]] = {}
|
||||||
self.set_snap_to_grid_size(snap_to_grid_size)
|
self.set_snap_to_grid_size(snap_to_grid_size)
|
||||||
self.scene = GraphGraphicsScene(self)
|
self.scene = GraphGraphicsScene(self)
|
||||||
self.scene.setSceneRect(-SCENE_SIZE / 2, -SCENE_SIZE / 2, SCENE_SIZE, SCENE_SIZE)
|
self.scene.setSceneRect(-SCENE_SIZE / 2, -SCENE_SIZE / 2, SCENE_SIZE, SCENE_SIZE)
|
||||||
@@ -185,6 +245,7 @@ class GraphEditorWidget(QWidget):
|
|||||||
self._component_items = {}
|
self._component_items = {}
|
||||||
self._component_bounds = {}
|
self._component_bounds = {}
|
||||||
self._connection_items = {}
|
self._connection_items = {}
|
||||||
|
self._connection_point_items = {}
|
||||||
self.scene.clear()
|
self.scene.clear()
|
||||||
if component is None:
|
if component is None:
|
||||||
return
|
return
|
||||||
@@ -213,10 +274,12 @@ class GraphEditorWidget(QWidget):
|
|||||||
tick_at_source = False
|
tick_at_source = False
|
||||||
elif connection.causality is BondCausality.FLOW_OUT:
|
elif connection.causality is BondCausality.FLOW_OUT:
|
||||||
tick_at_source = True
|
tick_at_source = True
|
||||||
connection_item = GraphConnectionItem([(0, 0), (1, 0)], half_arrow=isinstance(connection, BondConnection), tick_at_source=tick_at_source)
|
connection_item = GraphConnectionItem([(0, 0), (1, 0)], half_arrow=isinstance(connection, BondConnection), tick_at_source=tick_at_source, connection_id=connection_id, editor=self)
|
||||||
connection_item.setData(0, str(connection_id))
|
connection_item.setData(0, str(connection_id))
|
||||||
self._connection_items[connection_id] = connection_item
|
self._connection_items[connection_id] = connection_item
|
||||||
self.scene.addItem(connection_item)
|
self.scene.addItem(connection_item)
|
||||||
|
visual_connection = graph.connections.get(connection_id)
|
||||||
|
self._create_connection_point_items(connection_id, visual_connection.points[1:-1] if visual_connection is not None and len(visual_connection.points) >= 2 else [])
|
||||||
self.refresh_connections()
|
self.refresh_connections()
|
||||||
if component_changed:
|
if component_changed:
|
||||||
QTimer.singleShot(0, self.ui.actionZoomToFit.trigger)
|
QTimer.singleShot(0, self.ui.actionZoomToFit.trigger)
|
||||||
@@ -234,9 +297,8 @@ class GraphEditorWidget(QWidget):
|
|||||||
continue
|
continue
|
||||||
source_position = self._item_position(source_component)
|
source_position = self._item_position(source_component)
|
||||||
target_position = self._item_position(target_component)
|
target_position = self._item_position(target_component)
|
||||||
visual_connection = self._graph.connections.get(connection_id)
|
point_items = self._connection_point_items.get(connection_id, [])
|
||||||
points = list(visual_connection.points) if visual_connection is not None else []
|
points = [source_position, *((point.pos().x(), point.pos().y()) for point in point_items), target_position]
|
||||||
points = [source_position, target_position] if len(points) < 2 else [source_position, *points[1:-1], target_position]
|
|
||||||
points = self._straighten_direct_connection(points)
|
points = self._straighten_direct_connection(points)
|
||||||
source_bounds = self._component_bounds[source_component].translated(*source_position)
|
source_bounds = self._component_bounds[source_component].translated(*source_position)
|
||||||
target_bounds = self._component_bounds[target_component].translated(*target_position)
|
target_bounds = self._component_bounds[target_component].translated(*target_position)
|
||||||
@@ -246,6 +308,81 @@ class GraphEditorWidget(QWidget):
|
|||||||
tick_at_source = False if connection.causality is BondCausality.EFFORT_OUT else True if connection.causality is BondCausality.FLOW_OUT else None
|
tick_at_source = False if connection.causality is BondCausality.EFFORT_OUT else True if connection.causality is BondCausality.FLOW_OUT else None
|
||||||
item.setPath(item._connection_path(points, isinstance(connection, BondConnection), tick_at_source))
|
item.setPath(item._connection_path(points, isinstance(connection, BondConnection), tick_at_source))
|
||||||
|
|
||||||
|
def add_connection_point(self, connection_id: ConnectionID, scene_position: QPointF) -> None:
|
||||||
|
points = self._connection_metadata_points(connection_id)
|
||||||
|
position = self._snap_position(scene_position)
|
||||||
|
index = self._nearest_segment_index(points, position) + 1
|
||||||
|
points.insert(index, position)
|
||||||
|
self._request_connection_points_change(connection_id, points, "Add connection point")
|
||||||
|
|
||||||
|
def delete_connection_point(self, connection_id: ConnectionID, index: int) -> None:
|
||||||
|
points = self._connection_metadata_points(connection_id)
|
||||||
|
if 0 < index < len(points) - 1:
|
||||||
|
points.pop(index)
|
||||||
|
self._request_connection_points_change(connection_id, points, "Delete connection point")
|
||||||
|
|
||||||
|
def finish_connection_point_move(self, connection_id: ConnectionID) -> None:
|
||||||
|
self._request_connection_points_change(connection_id, self._connection_metadata_points(connection_id), "Move connection point")
|
||||||
|
|
||||||
|
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]
|
||||||
|
items = self._connection_point_items.get(connection_id, [])
|
||||||
|
if len(items) == len(interior_points):
|
||||||
|
for item, position in zip(items, interior_points):
|
||||||
|
item.setPos(*position)
|
||||||
|
else:
|
||||||
|
for item in items:
|
||||||
|
self.scene.removeItem(item)
|
||||||
|
self._create_connection_point_items(connection_id, interior_points)
|
||||||
|
self.refresh_connections()
|
||||||
|
|
||||||
|
def _create_connection_point_items(self, connection_id: ConnectionID, positions: list[tuple[int, int]]) -> None:
|
||||||
|
items = [GraphConnectionPointItem(connection_id, index, position, self) for index, position in enumerate(positions, 1)]
|
||||||
|
self._connection_point_items[connection_id] = items
|
||||||
|
for item in items:
|
||||||
|
self.scene.addItem(item)
|
||||||
|
|
||||||
|
def _connection_metadata_points(self, connection_id: ConnectionID) -> list[tuple[int, int]]:
|
||||||
|
component = self._component
|
||||||
|
if component is None:
|
||||||
|
return []
|
||||||
|
connection = component.implementation.graph.connections[connection_id]
|
||||||
|
port_owners = {port_id: component_id for component_id, child in component.implementation.graph.components.items() for port_id in child.interface.ports}
|
||||||
|
source = self._item_position(port_owners[connection.source])
|
||||||
|
target = self._item_position(port_owners[connection.target])
|
||||||
|
interior = [(round(item.pos().x()), round(item.pos().y())) for item in self._connection_point_items.get(connection_id, [])]
|
||||||
|
return [(round(source[0]), round(source[1])), *interior, (round(target[0]), round(target[1]))]
|
||||||
|
|
||||||
|
def _request_connection_points_change(self, connection_id: ConnectionID, points: list[tuple[int, int]], text: str) -> None:
|
||||||
|
if self._component is not None:
|
||||||
|
self.connection_points_change_requested.emit(self._component, connection_id, points, text)
|
||||||
|
|
||||||
|
def _snap_position(self, position: QPointF) -> tuple[int, int]:
|
||||||
|
size = self.snap_to_grid_size
|
||||||
|
return round(position.x() / size) * size, round(position.y() / size) * size
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _nearest_segment_index(points: list[tuple[int, int]], position: tuple[int, int]) -> int:
|
||||||
|
best_index = 0
|
||||||
|
best_distance = float("inf")
|
||||||
|
for index, (start, end) in enumerate(pairwise(points)):
|
||||||
|
dx = end[0] - start[0]
|
||||||
|
dy = end[1] - start[1]
|
||||||
|
length_squared = dx * dx + dy * dy
|
||||||
|
ratio = 0.0 if not length_squared else max(0.0, min(1.0, ((position[0] - start[0]) * dx + (position[1] - start[1]) * dy) / length_squared))
|
||||||
|
closest_x = start[0] + ratio * dx
|
||||||
|
closest_y = start[1] + ratio * dy
|
||||||
|
distance = (position[0] - closest_x) ** 2 + (position[1] - closest_y) ** 2
|
||||||
|
if distance < best_distance:
|
||||||
|
best_index = index
|
||||||
|
best_distance = distance
|
||||||
|
return best_index
|
||||||
|
|
||||||
def finish_component_move(self, component_id: ComponentID, position: tuple[int, int]) -> None:
|
def finish_component_move(self, component_id: ComponentID, position: tuple[int, int]) -> None:
|
||||||
if self._component is not None:
|
if self._component is not None:
|
||||||
self.component_move_requested.emit(self._component, component_id, position)
|
self.component_move_requested.emit(self._component, component_id, position)
|
||||||
|
|||||||
Reference in New Issue
Block a user