1333 lines
54 KiB
Python
1333 lines
54 KiB
Python
from copy import deepcopy
|
|
from pathlib import Path
|
|
from uuid import uuid4
|
|
|
|
from PySide6.QtCore import QObject, QPointF, Signal
|
|
from PySide6.QtGui import QUndoStack
|
|
|
|
from bedit.gui.controllers.commands import (
|
|
AddComponentCommand,
|
|
AddConnectionCommand,
|
|
AddAnnotationCommand,
|
|
AddInterfacePortCommand,
|
|
DeleteSelectionCommand,
|
|
DeleteAnnotationsCommand,
|
|
EditGraphItemCommand,
|
|
EditGraphParametersCommand,
|
|
EditSimulationSettingsCommand,
|
|
EditTextDefinitionCommand,
|
|
EditComponentAppearanceCommand,
|
|
EditComponentPropertiesCommand,
|
|
EditComponentParametersCommand,
|
|
MoveComponentCommand,
|
|
MoveInterfacePortCommand,
|
|
PasteSelectionCommand,
|
|
RenameConnectionCommand,
|
|
RenameInterfacePortCommand,
|
|
ReplaceSourceCommand,
|
|
RotateComponentsCommand,
|
|
SplitConnectionCommand,
|
|
)
|
|
from bedit.core.model import (
|
|
Annotation,
|
|
Component,
|
|
Connection,
|
|
Endpoint,
|
|
GraphDocument,
|
|
Icon,
|
|
Junction,
|
|
Parameter,
|
|
Port,
|
|
clone_component,
|
|
)
|
|
from bedit.core.simulation import Simulation
|
|
from bedit.core.port_types import PortTypeRegistry
|
|
from bedit.core.serializer import DocumentSerializer
|
|
|
|
|
|
class DocumentController(QObject):
|
|
documentReset = Signal()
|
|
documentOpenedChanged = Signal(bool)
|
|
activeGraphChanged = Signal()
|
|
componentAdded = Signal(str)
|
|
componentRemoved = Signal(str)
|
|
componentMoved = Signal(str, QPointF)
|
|
componentRotated = Signal(str, float)
|
|
componentPropertiesChanged = Signal(str)
|
|
textDefinitionChanged = Signal(str)
|
|
connectionAdded = Signal(str)
|
|
connectionRemoved = Signal(str)
|
|
graphItemChanged = Signal(str, str)
|
|
annotationAdded = Signal(str)
|
|
annotationRemoved = Signal(str)
|
|
interfaceChanged = Signal()
|
|
filePathChanged = Signal(object)
|
|
modifiedChanged = Signal(bool)
|
|
|
|
def __init__(self, parent=None, *, simulation: Simulation | None = None) -> None:
|
|
super().__init__(parent)
|
|
self.simulation = simulation or Simulation()
|
|
self.document: GraphDocument | None = None
|
|
self.active_component_id: str | None = None
|
|
self.file_path: Path | None = None
|
|
self.undo_stack = QUndoStack(self)
|
|
self.undo_stack.cleanChanged.connect(self._clean_changed)
|
|
|
|
def _clean_changed(self, clean: bool) -> None:
|
|
self.modifiedChanged.emit(not clean)
|
|
|
|
@property
|
|
def active_component(self) -> Component | None:
|
|
if self.document is None or self.active_component_id is None:
|
|
return None
|
|
return self.document.find_component(self.active_component_id)
|
|
|
|
@property
|
|
def active_graph(self):
|
|
component = self.active_component
|
|
if component is None or component.implementation_kind != "graph":
|
|
raise ValueError("There is no active graph")
|
|
return component.graph
|
|
|
|
def new_document(self) -> None:
|
|
self.document = GraphDocument.empty()
|
|
self.active_component_id = None
|
|
self.file_path = None
|
|
self.undo_stack.clear()
|
|
self.documentOpenedChanged.emit(True)
|
|
self.documentReset.emit()
|
|
self.activeGraphChanged.emit()
|
|
self.filePathChanged.emit(None)
|
|
|
|
def close_document(self) -> None:
|
|
self.document = None
|
|
self.active_component_id = None
|
|
self.file_path = None
|
|
self.undo_stack.clear()
|
|
self.documentOpenedChanged.emit(False)
|
|
self.documentReset.emit()
|
|
self.activeGraphChanged.emit()
|
|
self.filePathChanged.emit(None)
|
|
|
|
def load(self, path: Path) -> None:
|
|
self.document = DocumentSerializer.load(path)
|
|
self.active_component_id = next(iter(self.document.roots), None)
|
|
self.file_path = path
|
|
self.undo_stack.clear()
|
|
self.undo_stack.setClean()
|
|
self.documentOpenedChanged.emit(True)
|
|
self.documentReset.emit()
|
|
self.activeGraphChanged.emit()
|
|
self.filePathChanged.emit(path)
|
|
|
|
def save(self, path: Path | None = None) -> Path:
|
|
if self.document is None:
|
|
raise ValueError("There is no open document")
|
|
target = path or self.file_path
|
|
if target is None:
|
|
raise ValueError("No file path has been selected")
|
|
DocumentSerializer.save(self.document, target)
|
|
self.file_path = target
|
|
self.undo_stack.setClean()
|
|
self.filePathChanged.emit(target)
|
|
return target
|
|
|
|
def activate_component(self, component_id: str) -> None:
|
|
if self.document is None or self.document.find_component(component_id) is None:
|
|
return
|
|
self.active_component_id = component_id
|
|
self.activeGraphChanged.emit()
|
|
|
|
def navigate_up(self) -> None:
|
|
if self.document is None or self.active_component_id is None:
|
|
return
|
|
parent = self.document.find_parent(self.active_component_id)
|
|
if parent is not None:
|
|
self.activate_component(parent.id)
|
|
|
|
def breadcrumb(self) -> list[str]:
|
|
if self.document is None or self.active_component is None:
|
|
return []
|
|
names = [self.active_component.name]
|
|
current = self.active_component
|
|
while True:
|
|
parent = self.document.find_parent(current.id)
|
|
if parent is None:
|
|
break
|
|
names.append(parent.name)
|
|
current = parent
|
|
return list(reversed(names))
|
|
|
|
def add_root(self, kind: str) -> str:
|
|
if self.document is None:
|
|
raise ValueError("Open or create a document first")
|
|
number = len(self.document.roots) + 1
|
|
base_name = f"New {'Graph' if kind == 'graph' else 'Text'} Block "
|
|
component = Component(
|
|
id=str(uuid4()),
|
|
name=self._available_component_name(base_name, self.document.roots.values(), number),
|
|
implementation_kind=kind,
|
|
icon=Icon(text="Graph" if kind == "graph" else "Text"),
|
|
source={"equations": ""} if kind == "text" else {},
|
|
)
|
|
self.undo_stack.push(AddComponentCommand(self, None, component))
|
|
self.activate_component(component.id)
|
|
return component.id
|
|
|
|
def add_child(self, owner_id: str, kind: str) -> str:
|
|
if self.document is None:
|
|
raise ValueError("Open or create a document first")
|
|
owner = self.document.find_component(owner_id)
|
|
if owner is None or owner.implementation_kind != "graph":
|
|
raise ValueError("Children can only be added to graph components")
|
|
number = len(owner.graph.blocks) + 1
|
|
base_name = f"New {'Graph' if kind == 'graph' else 'Text'} Block "
|
|
component = Component(
|
|
id=str(uuid4()),
|
|
name=self._available_component_name(base_name, owner.graph.blocks.values(), number),
|
|
implementation_kind=kind,
|
|
icon=Icon(text="Graph" if kind == "graph" else "Text"),
|
|
source={"equations": ""} if kind == "text" else {},
|
|
)
|
|
self.undo_stack.push(AddComponentCommand(self, owner_id, component))
|
|
return component.id
|
|
|
|
def delete_component(self, component_id: str) -> None:
|
|
if self.document is None:
|
|
return
|
|
component = self.document.find_component(component_id)
|
|
if component is None:
|
|
return
|
|
parent = self.document.find_parent(component_id)
|
|
owner_id = parent.id if parent else None
|
|
connections = {}
|
|
if parent is not None:
|
|
connections = {
|
|
connection.id: connection
|
|
for connection in parent.graph.connections.values()
|
|
if component_id in (connection.source.block, connection.target.block)
|
|
}
|
|
self.undo_stack.push(
|
|
DeleteSelectionCommand(
|
|
self,
|
|
owner_id,
|
|
{component_id: component},
|
|
connections,
|
|
[],
|
|
[],
|
|
)
|
|
)
|
|
|
|
def add_component_copy(self, source: Component, position: QPointF) -> str:
|
|
if self.active_component is None or self.active_component.implementation_kind != "graph":
|
|
raise ValueError("Open a graph component before placing components")
|
|
component = clone_component(source)
|
|
component.name = self._available_component_name(
|
|
source.name, self.active_component.graph.blocks.values(), 0
|
|
)
|
|
component.x, component.y = position.x(), position.y()
|
|
self.undo_stack.push(AddComponentCommand(self, self.active_component_id, component))
|
|
return component.id
|
|
|
|
def move_component(self, component_id: str, old: QPointF, new: QPointF) -> None:
|
|
if old != new and self.active_component_id is not None:
|
|
self.undo_stack.push(
|
|
MoveComponentCommand(self, self.active_component_id, component_id, old, new)
|
|
)
|
|
|
|
def move_junction(self, junction_id: str, old: QPointF, new: QPointF) -> None:
|
|
if old != new and self.active_component_id is not None:
|
|
self.undo_stack.push(
|
|
EditGraphItemCommand(
|
|
self,
|
|
self.active_component_id,
|
|
"junction_geometry",
|
|
junction_id,
|
|
{"x": old.x(), "y": old.y()},
|
|
{"x": new.x(), "y": new.y()},
|
|
"Move connection junction",
|
|
)
|
|
)
|
|
|
|
def rotate_components(self, component_ids: set[str]) -> None:
|
|
if self.active_component is None or self.active_component_id is None:
|
|
return
|
|
rotations = {
|
|
component_id: (component.rotation, (component.rotation + 90.0) % 360.0)
|
|
for component_id in component_ids
|
|
if (component := self.active_component.graph.blocks.get(component_id)) is not None
|
|
}
|
|
if rotations:
|
|
self.undo_stack.push(RotateComponentsCommand(self, self.active_component_id, rotations))
|
|
|
|
def connect(
|
|
self,
|
|
source: Endpoint,
|
|
target: Endpoint,
|
|
*,
|
|
waypoints: list[QPointF] | None = None,
|
|
) -> str:
|
|
if self.active_component_id is None:
|
|
raise ValueError("There is no active graph")
|
|
source_port = self._port_for_endpoint(source, "source")
|
|
target_port = self._port_for_endpoint(target, "target")
|
|
if source_port is None or target_port is None:
|
|
raise ValueError("A connection endpoint no longer exists")
|
|
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
|
|
raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}")
|
|
if not self.endpoint_accepts_connection(source, "source"):
|
|
raise ValueError(
|
|
f"Port {source_port.name!r} already has a connection; enable multiple connections first"
|
|
)
|
|
if not self.endpoint_accepts_connection(target, "target"):
|
|
raise ValueError(
|
|
f"Port {target_port.name!r} already has a connection; enable multiple connections first"
|
|
)
|
|
connection = Connection(
|
|
str(uuid4()),
|
|
source,
|
|
target,
|
|
properties={
|
|
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
|
|
},
|
|
)
|
|
self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
|
|
return connection.id
|
|
|
|
def split_connection(
|
|
self,
|
|
connection_id: str,
|
|
position: QPointF,
|
|
first_waypoints: list[QPointF],
|
|
second_waypoints: list[QPointF],
|
|
) -> str:
|
|
if self.active_component_id is None:
|
|
raise ValueError("There is no active graph")
|
|
original = self.active_graph.connections.get(connection_id)
|
|
if original is None:
|
|
raise ValueError("The connection no longer exists")
|
|
port_type = self.connection_port_type(original)
|
|
junction = Junction(str(uuid4()), position.x(), position.y(), port_type)
|
|
first_properties = deepcopy(original.properties)
|
|
first_properties["waypoints"] = [
|
|
{"x": point.x(), "y": point.y()} for point in first_waypoints
|
|
]
|
|
second_properties = {
|
|
"waypoints": [{"x": point.x(), "y": point.y()} for point in second_waypoints]
|
|
}
|
|
first = Connection(
|
|
str(uuid4()),
|
|
original.source,
|
|
Endpoint(junction=junction.id),
|
|
original.name,
|
|
first_properties,
|
|
)
|
|
second = Connection(
|
|
str(uuid4()),
|
|
Endpoint(junction=junction.id),
|
|
original.target,
|
|
"",
|
|
second_properties,
|
|
)
|
|
self.undo_stack.push(
|
|
SplitConnectionCommand(
|
|
self,
|
|
self.active_component_id,
|
|
original,
|
|
junction,
|
|
first,
|
|
second,
|
|
)
|
|
)
|
|
return junction.id
|
|
|
|
def add_annotation(
|
|
self,
|
|
kind: str,
|
|
start: QPointF,
|
|
end: QPointF,
|
|
*,
|
|
text: str = "",
|
|
waypoints: list[QPointF] | None = None,
|
|
) -> str:
|
|
if self.active_component_id is None:
|
|
raise ValueError("There is no active graph")
|
|
if kind != "line":
|
|
left, right = sorted((start.x(), end.x()))
|
|
top, bottom = sorted((start.y(), end.y()))
|
|
start, end = QPointF(left, top), QPointF(right, bottom)
|
|
style = {
|
|
"stroke": "#303030",
|
|
"lineWidth": 1.5,
|
|
"lineStyle": "solid",
|
|
"fill": "none" if kind in {"line", "text"} else "#dbeafe",
|
|
}
|
|
if kind == "box":
|
|
style["cornerRadius"] = 0.0
|
|
if kind == "text":
|
|
style.update({"fontSize": 12.0, "color": "#202020"})
|
|
annotation = Annotation(
|
|
id=str(uuid4()),
|
|
kind=kind,
|
|
x=start.x(),
|
|
y=start.y(),
|
|
width=end.x() - start.x(),
|
|
height=end.y() - start.y(),
|
|
text=text,
|
|
layer=-1,
|
|
properties={
|
|
**style,
|
|
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
|
|
},
|
|
)
|
|
self.undo_stack.push(AddAnnotationCommand(self, self.active_component_id, annotation))
|
|
return annotation.id
|
|
|
|
def edit_simulation_settings(self, settings: dict) -> None:
|
|
component = self.active_component
|
|
if component is None or component.implementation_kind != "graph":
|
|
raise ValueError("Open a graph component before editing simulation settings")
|
|
old = deepcopy(component.graph.simulation_settings)
|
|
new = deepcopy(settings)
|
|
if old != new:
|
|
self.undo_stack.push(
|
|
EditSimulationSettingsCommand(self, component.id, old, new)
|
|
)
|
|
|
|
def edit_graph_parameter_values(
|
|
self, root_id: str, values: dict[str, dict[str, str]]
|
|
) -> None:
|
|
root = self.document.find_component(root_id) if self.document else None
|
|
if root is None:
|
|
return
|
|
subtree_ids = {component.id for component in self._component_subtree(root)}
|
|
if not set(values) <= subtree_ids:
|
|
raise ValueError("Parameter changes contain a component outside the active graph")
|
|
|
|
old: dict[str, list[dict]] = {}
|
|
new: dict[str, list[dict]] = {}
|
|
for component_id, parameter_values in values.items():
|
|
component = self.document.find_component(component_id)
|
|
known_ids = {parameter.id for parameter in component.parameters}
|
|
if not set(parameter_values) <= known_ids:
|
|
raise ValueError(f"Component {component.name!r} contains an unknown parameter")
|
|
updated = deepcopy(component.parameters)
|
|
for parameter in updated:
|
|
if parameter.id in parameter_values:
|
|
parameter.value = parameter_values[parameter.id]
|
|
old[component_id] = [parameter.to_dict() for parameter in component.parameters]
|
|
new[component_id] = [parameter.to_dict() for parameter in updated]
|
|
|
|
if old != new:
|
|
self.undo_stack.push(EditGraphParametersCommand(self, old, new))
|
|
|
|
def compile_active_graph(self) -> None:
|
|
component = self.active_component
|
|
if component is None or component.implementation_kind != "graph":
|
|
raise ValueError("Open a graph component before compiling")
|
|
self.simulation.compile(component.to_dict())
|
|
|
|
def run_simulation(self) -> None:
|
|
component = self.active_component
|
|
if component is None or component.implementation_kind != "graph":
|
|
raise ValueError("Open a graph component before running a simulation")
|
|
self.simulation.run_simulation()
|
|
|
|
def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None:
|
|
item = (
|
|
self.active_graph.connections
|
|
if item_kind == "connection"
|
|
else self.active_graph.annotations
|
|
).get(item_id)
|
|
if item is None or self.active_component_id is None:
|
|
return
|
|
old = deepcopy(item.properties)
|
|
new = deepcopy(old)
|
|
new["waypoints"] = [{"x": point.x(), "y": point.y()} for point in waypoints]
|
|
if old != new:
|
|
self.undo_stack.push(
|
|
EditGraphItemCommand(
|
|
self, self.active_component_id, item_kind, item_id, old, new, "Edit line nodes"
|
|
)
|
|
)
|
|
|
|
def set_annotation_geometry(self, annotation_id: str, old: dict, new: dict) -> None:
|
|
if old != new and self.active_component_id is not None:
|
|
self.undo_stack.push(
|
|
EditGraphItemCommand(
|
|
self,
|
|
self.active_component_id,
|
|
"annotation_geometry",
|
|
annotation_id,
|
|
old,
|
|
new,
|
|
"Move annotation",
|
|
)
|
|
)
|
|
|
|
def edit_annotation(self, annotation_id: str, values: dict) -> None:
|
|
annotation = self.active_graph.annotations.get(annotation_id)
|
|
if annotation is None or self.active_component_id is None:
|
|
return
|
|
old = annotation.to_dict()
|
|
if old != values:
|
|
self.undo_stack.push(
|
|
EditGraphItemCommand(
|
|
self,
|
|
self.active_component_id,
|
|
"annotation_data",
|
|
annotation_id,
|
|
old,
|
|
deepcopy(values),
|
|
"Edit shape",
|
|
)
|
|
)
|
|
|
|
def reorder_annotations(self, annotation_ids: set[str], operation: str) -> None:
|
|
if not annotation_ids or self.active_component_id is None:
|
|
return
|
|
graph = self.active_graph
|
|
layers = [item.layer for item in graph.annotations.values()]
|
|
minimum, maximum = min(layers, default=-1), max(layers, default=1)
|
|
for annotation_id in annotation_ids:
|
|
item = graph.annotations.get(annotation_id)
|
|
if item is None:
|
|
continue
|
|
old = {"layer": item.layer}
|
|
forward = item.layer + 1
|
|
backward = item.layer - 1
|
|
if forward == 0:
|
|
forward = 1
|
|
if backward == 0:
|
|
backward = -1
|
|
layer = {
|
|
"forward": forward,
|
|
"backward": backward,
|
|
"front": max(1, maximum + 1),
|
|
"back": min(-1, minimum - 1),
|
|
}[operation]
|
|
self.undo_stack.push(
|
|
EditGraphItemCommand(
|
|
self,
|
|
self.active_component_id,
|
|
"annotation_layer",
|
|
annotation_id,
|
|
old,
|
|
{"layer": layer},
|
|
"Reorder annotation",
|
|
)
|
|
)
|
|
|
|
def delete_annotations(self, annotation_ids: set[str]) -> None:
|
|
items = {
|
|
key: self.active_graph.annotations[key]
|
|
for key in annotation_ids
|
|
if key in self.active_graph.annotations
|
|
}
|
|
if items and self.active_component_id is not None:
|
|
self.undo_stack.push(DeleteAnnotationsCommand(self, self.active_component_id, items))
|
|
|
|
def _port_for_endpoint(self, endpoint: Endpoint, role: str) -> Port | None:
|
|
owner = self.active_component
|
|
if owner is None:
|
|
return None
|
|
if endpoint.junction is not None:
|
|
junction = owner.graph.junctions.get(endpoint.junction)
|
|
if junction is None:
|
|
return None
|
|
return Port(
|
|
junction.id,
|
|
"Junction",
|
|
type=junction.type,
|
|
allows_multiple_connections=role == "source",
|
|
)
|
|
if endpoint.interface is not None:
|
|
ports = owner.inputs if role == "source" else owner.outputs
|
|
else:
|
|
component = owner.graph.blocks.get(endpoint.block or "")
|
|
if component is None:
|
|
return None
|
|
ports = component.outputs if role == "source" else component.inputs
|
|
return next(
|
|
(port for port in ports if port.id == (endpoint.interface or endpoint.port)), None
|
|
)
|
|
|
|
def connection_port_type(self, connection: Connection) -> str:
|
|
port = self._port_for_endpoint(connection.source, "source")
|
|
return port.type if port is not None else "signal"
|
|
|
|
def endpoint_accepts_connection(self, endpoint: Endpoint, role: str) -> bool:
|
|
port = self._port_for_endpoint(endpoint, role)
|
|
if port is None:
|
|
return False
|
|
if role == "source" or port.allows_multiple_connections:
|
|
return True
|
|
return not any(
|
|
endpoint == (connection.source if role == "source" else connection.target)
|
|
for connection in self.active_graph.connections.values()
|
|
)
|
|
|
|
def add_interface_port(self, direction: str, position: QPointF) -> str:
|
|
component = self.active_component
|
|
if component is None or component.implementation_kind != "graph":
|
|
raise ValueError("Open a graph component before adding an interface")
|
|
ports = component.inputs if direction == "input" else component.outputs
|
|
port = Port(
|
|
id=f"{direction}-{uuid4().hex[:8]}",
|
|
name=f"{direction.title()} {len(ports) + 1}",
|
|
x=position.x(),
|
|
y=position.y(),
|
|
)
|
|
self.undo_stack.push(AddInterfacePortCommand(self, component.id, direction, port))
|
|
return port.id
|
|
|
|
def move_interface_port(self, port_id: str, old: QPointF, new: QPointF) -> None:
|
|
if old != new and self.active_component_id is not None:
|
|
self.undo_stack.push(
|
|
MoveInterfacePortCommand(self, self.active_component_id, port_id, old, new)
|
|
)
|
|
|
|
def rename_interface_port(self, port_id: str, name: str) -> None:
|
|
owner = self.active_component
|
|
if owner is None:
|
|
return
|
|
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
|
if port is not None and port.name != name:
|
|
self.undo_stack.push(
|
|
RenameInterfacePortCommand(self, owner.id, port_id, port.name, name)
|
|
)
|
|
|
|
def rename_connection(self, connection_id: str, name: str) -> None:
|
|
owner = self.active_component
|
|
if owner is None or owner.implementation_kind != "graph":
|
|
return
|
|
connection = owner.graph.connections.get(connection_id)
|
|
if connection is not None and connection.name != name:
|
|
self.undo_stack.push(
|
|
RenameConnectionCommand(self, owner.id, connection_id, connection.name, name)
|
|
)
|
|
|
|
def replace_active_source(self, source: dict) -> None:
|
|
component = self.active_component
|
|
if component is None or component.implementation_kind != "text":
|
|
raise ValueError("Only text-defined components have source JSON")
|
|
self.undo_stack.push(
|
|
ReplaceSourceCommand(
|
|
self,
|
|
component.id,
|
|
deepcopy(component.source),
|
|
deepcopy(source),
|
|
)
|
|
)
|
|
|
|
def replace_active_text_definition(
|
|
self,
|
|
inputs: list[Port],
|
|
outputs: list[Port],
|
|
equations: str,
|
|
parameters: list[Parameter],
|
|
) -> None:
|
|
component = self.active_component
|
|
if component is None or component.implementation_kind != "text":
|
|
raise ValueError("Only text-defined components can be edited here")
|
|
input_ids = [port.id for port in inputs]
|
|
output_ids = [port.id for port in outputs]
|
|
if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids):
|
|
raise ValueError("Input and output IDs must be unique")
|
|
if any(not port.name.strip() for port in (*inputs, *outputs)):
|
|
raise ValueError("Every port must have a name")
|
|
parameter_ids = [parameter.id for parameter in parameters]
|
|
if len(set(parameter_ids)) != len(parameter_ids):
|
|
raise ValueError("Parameter IDs must be unique")
|
|
if any(not parameter.name.strip() for parameter in parameters):
|
|
raise ValueError("Every parameter must have a name")
|
|
if self.document is not None:
|
|
parent = self.document.find_parent(component.id)
|
|
if parent is not None:
|
|
for connection in parent.graph.connections.values():
|
|
if (
|
|
connection.target.block == component.id
|
|
and connection.target.port not in input_ids
|
|
):
|
|
raise ValueError(
|
|
f"Input {connection.target.port!r} is still connected in the containing graph"
|
|
)
|
|
if (
|
|
connection.source.block == component.id
|
|
and connection.source.port not in output_ids
|
|
):
|
|
raise ValueError(
|
|
f"Output {connection.source.port!r} is still connected in the containing graph"
|
|
)
|
|
old = {
|
|
"inputs": [port.to_dict() for port in component.inputs],
|
|
"outputs": [port.to_dict() for port in component.outputs],
|
|
"source": deepcopy(component.source),
|
|
"parameters": [parameter.to_dict() for parameter in component.parameters],
|
|
}
|
|
new = {
|
|
"inputs": [port.to_dict() for port in inputs],
|
|
"outputs": [port.to_dict() for port in outputs],
|
|
"source": {
|
|
"equations": equations,
|
|
},
|
|
"parameters": [parameter.to_dict() for parameter in parameters],
|
|
}
|
|
if old != new:
|
|
candidate = deepcopy(self.document)
|
|
candidate_component = candidate.find_component(component.id)
|
|
candidate_component.inputs = deepcopy(inputs)
|
|
candidate_component.outputs = deepcopy(outputs)
|
|
candidate_component.source = deepcopy(new["source"])
|
|
candidate_component.parameters = deepcopy(parameters)
|
|
candidate.validate()
|
|
self.undo_stack.push(EditTextDefinitionCommand(self, component.id, old, new))
|
|
|
|
def edit_component_appearance(
|
|
self,
|
|
component_id: str,
|
|
name: str,
|
|
icon: Icon,
|
|
inputs: list[Port],
|
|
outputs: list[Port],
|
|
show_subtree: bool,
|
|
show_name: bool,
|
|
) -> None:
|
|
if self.document is None:
|
|
return
|
|
component = self.document.find_component(component_id)
|
|
if component is None:
|
|
return
|
|
siblings = self._component_siblings(component_id)
|
|
if any(item.id != component_id and item.name == name for item in siblings):
|
|
raise ValueError(f"A component named {name!r} already exists at this level")
|
|
old = {
|
|
"name": component.name,
|
|
"icon": component.icon.to_dict(),
|
|
"inputs": [port.to_dict() for port in component.inputs],
|
|
"outputs": [port.to_dict() for port in component.outputs],
|
|
"show_subtree": component.show_subtree_in_library,
|
|
"properties": deepcopy(component.properties),
|
|
}
|
|
properties = deepcopy(component.properties)
|
|
was_visible = bool(properties.get("showName", False))
|
|
properties["showName"] = show_name
|
|
if show_name and not was_visible:
|
|
properties.pop("nameLabelPosition", None)
|
|
new = {
|
|
"name": name,
|
|
"icon": icon.to_dict(),
|
|
"inputs": [port.to_dict() for port in inputs],
|
|
"outputs": [port.to_dict() for port in outputs],
|
|
"show_subtree": show_subtree,
|
|
"properties": properties,
|
|
}
|
|
if old != new:
|
|
candidate = deepcopy(self.document)
|
|
candidate_component = candidate.find_component(component_id)
|
|
candidate_component.name = name
|
|
candidate_component.icon = Icon.from_dict(icon.to_dict())
|
|
candidate_component.inputs = deepcopy(inputs)
|
|
candidate_component.outputs = deepcopy(outputs)
|
|
candidate_component.properties = deepcopy(properties)
|
|
candidate.validate()
|
|
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
|
|
|
def move_component_name_label(self, component_id: str, position: QPointF) -> None:
|
|
if self.document is None:
|
|
return
|
|
component = self.document.find_component(component_id)
|
|
if component is None:
|
|
return
|
|
old = deepcopy(component.properties)
|
|
new = deepcopy(old)
|
|
new["nameLabelPosition"] = {"x": position.x(), "y": position.y()}
|
|
if old != new:
|
|
self.undo_stack.push(
|
|
EditComponentPropertiesCommand(
|
|
self, component_id, old, new, "Move component name"
|
|
)
|
|
)
|
|
|
|
def edit_connection_options(
|
|
self, connection_id: str, name: str, show_name: bool
|
|
) -> None:
|
|
owner = self.active_component
|
|
if owner is None or self.active_component_id is None:
|
|
return
|
|
connection = owner.graph.connections.get(connection_id)
|
|
if connection is None:
|
|
return
|
|
old = {"name": connection.name, "properties": deepcopy(connection.properties)}
|
|
properties = deepcopy(connection.properties)
|
|
was_visible = bool(properties.get("showName", False))
|
|
properties["showName"] = show_name
|
|
if show_name and not was_visible:
|
|
properties.pop("nameLabelPosition", None)
|
|
new = {"name": name, "properties": properties}
|
|
if old != new:
|
|
self.undo_stack.push(
|
|
EditGraphItemCommand(
|
|
self,
|
|
self.active_component_id,
|
|
"connection_data",
|
|
connection_id,
|
|
old,
|
|
new,
|
|
"Edit connection options",
|
|
)
|
|
)
|
|
|
|
def move_connection_name_label(self, connection_id: str, position: QPointF) -> None:
|
|
connection = self.active_graph.connections.get(connection_id)
|
|
if connection is None or self.active_component_id is None:
|
|
return
|
|
old = deepcopy(connection.properties)
|
|
new = deepcopy(old)
|
|
new["nameLabelPosition"] = {"x": position.x(), "y": position.y()}
|
|
if old != new:
|
|
self.undo_stack.push(
|
|
EditGraphItemCommand(
|
|
self,
|
|
self.active_component_id,
|
|
"connection",
|
|
connection_id,
|
|
old,
|
|
new,
|
|
"Move connection name",
|
|
)
|
|
)
|
|
|
|
def edit_component_ports(
|
|
self, component_id: str, inputs: list[Port], outputs: list[Port]
|
|
) -> None:
|
|
if self.document is None:
|
|
return
|
|
component = self.document.find_component(component_id)
|
|
if component is None:
|
|
return
|
|
input_ids = {port.id for port in inputs}
|
|
output_ids = {port.id for port in outputs}
|
|
parent = self.document.find_parent(component_id)
|
|
if parent is not None:
|
|
for connection in parent.graph.connections.values():
|
|
if (
|
|
connection.target.block == component_id
|
|
and connection.target.port not in input_ids
|
|
):
|
|
raise ValueError("An input cannot be removed or reoriented while connected")
|
|
if (
|
|
connection.source.block == component_id
|
|
and connection.source.port not in output_ids
|
|
):
|
|
raise ValueError("An output cannot be removed or reoriented while connected")
|
|
for connection in component.graph.connections.values():
|
|
if connection.source.interface and connection.source.interface not in input_ids:
|
|
raise ValueError("An interface input cannot be removed while connected")
|
|
if connection.target.interface and connection.target.interface not in output_ids:
|
|
raise ValueError("An interface output cannot be removed while connected")
|
|
candidate = deepcopy(self.document)
|
|
candidate_component = candidate.find_component(component_id)
|
|
candidate_component.inputs = deepcopy(inputs)
|
|
candidate_component.outputs = deepcopy(outputs)
|
|
candidate.validate()
|
|
old = {
|
|
"name": component.name,
|
|
"icon": component.icon.to_dict(),
|
|
"inputs": [port.to_dict() for port in component.inputs],
|
|
"outputs": [port.to_dict() for port in component.outputs],
|
|
"show_subtree": component.show_subtree_in_library,
|
|
"properties": deepcopy(component.properties),
|
|
}
|
|
new = {
|
|
**old,
|
|
"inputs": [port.to_dict() for port in inputs],
|
|
"outputs": [port.to_dict() for port in outputs],
|
|
}
|
|
if old != new:
|
|
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
|
|
|
def edit_component_parameters(
|
|
self, component_id: str, parameters: list[Parameter]
|
|
) -> None:
|
|
component = self.document.find_component(component_id) if self.document else None
|
|
if component is None:
|
|
return
|
|
ids = [parameter.id for parameter in parameters]
|
|
names = [parameter.name for parameter in parameters]
|
|
if len(set(ids)) != len(ids):
|
|
raise ValueError("Parameter IDs must be unique")
|
|
if len(set(names)) != len(names):
|
|
raise ValueError("Parameter names must be unique")
|
|
if any(not name.strip() for name in names):
|
|
raise ValueError("Every parameter must have a name")
|
|
old = [parameter.to_dict() for parameter in component.parameters]
|
|
new = [parameter.to_dict() for parameter in parameters]
|
|
if old != new:
|
|
self.undo_stack.push(
|
|
EditComponentParametersCommand(self, component_id, old, new)
|
|
)
|
|
|
|
def delete_selection(
|
|
self,
|
|
block_ids: set[str],
|
|
connection_ids: set[str],
|
|
input_ids: set[str],
|
|
output_ids: set[str],
|
|
) -> None:
|
|
component = self.active_component
|
|
if component is None or component.implementation_kind != "graph":
|
|
return
|
|
graph = component.graph
|
|
all_connection_ids = set(connection_ids)
|
|
for connection in graph.connections.values():
|
|
if (
|
|
connection.source.block in block_ids
|
|
or connection.target.block in block_ids
|
|
or connection.source.interface in input_ids
|
|
or connection.target.interface in output_ids
|
|
):
|
|
all_connection_ids.add(connection.id)
|
|
blocks = {
|
|
block_id: graph.blocks[block_id] for block_id in block_ids if block_id in graph.blocks
|
|
}
|
|
connections = {
|
|
connection_id: graph.connections[connection_id]
|
|
for connection_id in all_connection_ids
|
|
if connection_id in graph.connections
|
|
}
|
|
inputs = [port for port in component.inputs if port.id in input_ids]
|
|
outputs = [port for port in component.outputs if port.id in output_ids]
|
|
if not (blocks or connections or inputs or outputs):
|
|
return
|
|
self.undo_stack.push(
|
|
DeleteSelectionCommand(
|
|
self,
|
|
component.id,
|
|
blocks,
|
|
connections,
|
|
inputs,
|
|
outputs,
|
|
)
|
|
)
|
|
|
|
def paste_selection(
|
|
self,
|
|
source_components: list[Component],
|
|
source_connections: list[Connection],
|
|
offset: QPointF,
|
|
) -> list[str]:
|
|
owner = self.active_component
|
|
if owner is None or owner.implementation_kind != "graph":
|
|
return []
|
|
pairs = [(source, clone_component(source)) for source in source_components]
|
|
id_map = {source.id: clone.id for source, clone in pairs}
|
|
blocks = {}
|
|
used = list(owner.graph.blocks.values())
|
|
for source, clone in pairs:
|
|
clone.name = self._available_component_name(source.name, used, 0)
|
|
clone.x += offset.x()
|
|
clone.y += offset.y()
|
|
blocks[clone.id] = clone
|
|
used.append(clone)
|
|
connections = {}
|
|
for source in source_connections:
|
|
if source.source.block not in id_map or source.target.block not in id_map:
|
|
continue
|
|
properties = deepcopy(source.properties)
|
|
for point in properties.get("waypoints", []):
|
|
if isinstance(point, dict):
|
|
point["x"] = float(point.get("x", 0)) + offset.x()
|
|
point["y"] = float(point.get("y", 0)) + offset.y()
|
|
connection = Connection(
|
|
id=str(uuid4()),
|
|
source=Endpoint(block=id_map[source.source.block], port=source.source.port),
|
|
target=Endpoint(block=id_map[source.target.block], port=source.target.port),
|
|
name=source.name,
|
|
properties=properties,
|
|
)
|
|
connections[connection.id] = connection
|
|
if blocks:
|
|
self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections))
|
|
return list(blocks)
|
|
|
|
def _graph_for(self, owner_id: str):
|
|
if self.document is None:
|
|
raise ValueError("There is no open document")
|
|
owner = self.document.find_component(owner_id)
|
|
if owner is None:
|
|
raise ValueError("The containing component is no longer in the document")
|
|
return owner.graph
|
|
|
|
@staticmethod
|
|
def _available_component_name(
|
|
base: str, components, start: int = 0
|
|
) -> str:
|
|
used = {component.name for component in components}
|
|
number = start
|
|
while f"{base}{number}" in used:
|
|
number += 1
|
|
return f"{base}{number}"
|
|
|
|
def _component_siblings(self, component_id: str):
|
|
if self.document is None:
|
|
return ()
|
|
parent = self.document.find_parent(component_id)
|
|
return (
|
|
parent.graph.blocks.values()
|
|
if parent is not None
|
|
else self.document.roots.values()
|
|
)
|
|
|
|
def _insert_component(self, owner_id: str | None, component: Component) -> None:
|
|
if self.document is None:
|
|
raise ValueError("There is no open document")
|
|
if owner_id is None:
|
|
self.document.roots[component.id] = component
|
|
else:
|
|
self._graph_for(owner_id).blocks[component.id] = component
|
|
if owner_id == self.active_component_id:
|
|
self.componentAdded.emit(component.id)
|
|
self.documentReset.emit()
|
|
|
|
def _remove_component(self, owner_id: str | None, component_id: str) -> None:
|
|
if self.document is None:
|
|
return
|
|
if owner_id is None:
|
|
self.document.roots.pop(component_id, None)
|
|
if self.active_component_id == component_id:
|
|
self.active_component_id = next(iter(self.document.roots), None)
|
|
self.activeGraphChanged.emit()
|
|
else:
|
|
self._graph_for(owner_id).blocks.pop(component_id, None)
|
|
if owner_id == self.active_component_id:
|
|
self.componentRemoved.emit(component_id)
|
|
self.documentReset.emit()
|
|
|
|
def _move_component(self, owner_id: str, component_id: str, position: QPointF) -> None:
|
|
component = self._graph_for(owner_id).blocks[component_id]
|
|
component.x, component.y = position.x(), position.y()
|
|
if owner_id == self.active_component_id:
|
|
self.componentMoved.emit(component_id, position)
|
|
self.documentReset.emit()
|
|
|
|
def _rotate_component(self, owner_id: str, component_id: str, rotation: float) -> None:
|
|
component = self._graph_for(owner_id).blocks.get(component_id)
|
|
if component is None:
|
|
return
|
|
component.rotation = rotation
|
|
if owner_id == self.active_component_id:
|
|
self.componentRotated.emit(component_id, rotation)
|
|
|
|
def _insert_connection(self, owner_id: str, connection: Connection) -> None:
|
|
self._graph_for(owner_id).connections[connection.id] = connection
|
|
if owner_id == self.active_component_id:
|
|
self.connectionAdded.emit(connection.id)
|
|
self.documentReset.emit()
|
|
|
|
def _split_connection(
|
|
self,
|
|
owner_id: str,
|
|
original_id: str,
|
|
junction: Junction,
|
|
first: Connection,
|
|
second: Connection,
|
|
) -> None:
|
|
graph = self._graph_for(owner_id)
|
|
graph.connections.pop(original_id, None)
|
|
graph.junctions[junction.id] = junction
|
|
graph.connections[first.id] = first
|
|
graph.connections[second.id] = second
|
|
self.documentReset.emit()
|
|
|
|
def _restore_split_connection(
|
|
self,
|
|
owner_id: str,
|
|
original: Connection,
|
|
junction_id: str,
|
|
first_id: str,
|
|
second_id: str,
|
|
) -> None:
|
|
graph = self._graph_for(owner_id)
|
|
graph.connections.pop(first_id, None)
|
|
graph.connections.pop(second_id, None)
|
|
graph.junctions.pop(junction_id, None)
|
|
graph.connections[original.id] = original
|
|
self.documentReset.emit()
|
|
|
|
def _remove_connection(self, owner_id: str, connection_id: str) -> None:
|
|
self._graph_for(owner_id).connections.pop(connection_id, None)
|
|
if owner_id == self.active_component_id:
|
|
self.connectionRemoved.emit(connection_id)
|
|
self.documentReset.emit()
|
|
|
|
def _insert_annotation(self, owner_id: str, annotation: Annotation) -> None:
|
|
self._graph_for(owner_id).annotations[annotation.id] = annotation
|
|
if owner_id == self.active_component_id:
|
|
self.annotationAdded.emit(annotation.id)
|
|
self.documentReset.emit()
|
|
|
|
def _remove_annotation(self, owner_id: str, annotation_id: str) -> None:
|
|
self._graph_for(owner_id).annotations.pop(annotation_id, None)
|
|
if owner_id == self.active_component_id:
|
|
self.annotationRemoved.emit(annotation_id)
|
|
self.documentReset.emit()
|
|
|
|
def _set_graph_item_data(
|
|
self, owner_id: str, item_kind: str, item_id: str, values: dict
|
|
) -> None:
|
|
graph = self._graph_for(owner_id)
|
|
if item_kind == "junction_geometry":
|
|
item = graph.junctions.get(item_id)
|
|
if item is not None:
|
|
item.x, item.y = float(values["x"]), float(values["y"])
|
|
elif item_kind == "connection_data":
|
|
item = graph.connections.get(item_id)
|
|
if item is not None:
|
|
item.name = values["name"]
|
|
item.properties = deepcopy(values["properties"])
|
|
elif item_kind == "connection":
|
|
item = graph.connections.get(item_id)
|
|
if item is not None:
|
|
item.properties = deepcopy(values)
|
|
else:
|
|
item = graph.annotations.get(item_id)
|
|
if item is not None:
|
|
if item_kind == "annotation_geometry":
|
|
item.x, item.y = float(values["x"]), float(values["y"])
|
|
item.width, item.height = float(values["width"]), float(values["height"])
|
|
elif item_kind == "annotation_layer":
|
|
item.layer = int(values["layer"])
|
|
elif item_kind == "annotation_data":
|
|
replacement = Annotation.from_dict(values)
|
|
item.kind = replacement.kind
|
|
item.x, item.y = replacement.x, replacement.y
|
|
item.width, item.height = replacement.width, replacement.height
|
|
item.text, item.layer = replacement.text, replacement.layer
|
|
item.properties = replacement.properties
|
|
else:
|
|
item.properties = deepcopy(values)
|
|
if owner_id == self.active_component_id:
|
|
self.graphItemChanged.emit(item_kind, item_id)
|
|
|
|
def _set_simulation_settings(self, owner_id: str, settings: dict) -> None:
|
|
owner = self.document.find_component(owner_id) if self.document else None
|
|
if owner is not None and owner.implementation_kind == "graph":
|
|
owner.graph.simulation_settings = deepcopy(settings)
|
|
self.documentReset.emit()
|
|
|
|
def _set_graph_parameters(self, values: dict[str, list[dict]]) -> None:
|
|
if self.document is None:
|
|
return
|
|
changed_text_components: list[str] = []
|
|
for component_id, parameters in values.items():
|
|
component = self.document.find_component(component_id)
|
|
if component is None:
|
|
continue
|
|
component.parameters = [Parameter.from_dict(item) for item in parameters]
|
|
if component.implementation_kind == "text":
|
|
changed_text_components.append(component_id)
|
|
self.document.validate()
|
|
self.documentReset.emit()
|
|
for component_id in changed_text_components:
|
|
self.textDefinitionChanged.emit(component_id)
|
|
|
|
def _replace_component(self, old_id: str, replacement: Component) -> None:
|
|
if self.document is None:
|
|
return
|
|
was_active = self.active_component_id == old_id
|
|
if old_id in self.document.roots:
|
|
self.document.roots.pop(old_id)
|
|
self.document.roots[replacement.id] = replacement
|
|
else:
|
|
parent = self.document.find_parent(old_id)
|
|
if parent is None:
|
|
raise ValueError("The component is no longer in this document")
|
|
parent.graph.blocks.pop(old_id)
|
|
parent.graph.blocks[replacement.id] = replacement
|
|
if was_active:
|
|
self.active_component_id = replacement.id
|
|
self.document.validate()
|
|
self.documentReset.emit()
|
|
if was_active:
|
|
self.activeGraphChanged.emit()
|
|
|
|
def _insert_interface_port(self, owner_id: str, direction: str, port: Port) -> None:
|
|
if self.document is None:
|
|
return
|
|
owner = self.document.find_component(owner_id)
|
|
if owner is None:
|
|
return
|
|
ports = owner.inputs if direction == "input" else owner.outputs
|
|
if all(existing.id != port.id for existing in ports):
|
|
ports.append(port)
|
|
self.interfaceChanged.emit()
|
|
self.documentReset.emit()
|
|
|
|
def _remove_interface_port(self, owner_id: str, direction: str, port_id: str) -> None:
|
|
if self.document is None:
|
|
return
|
|
owner = self.document.find_component(owner_id)
|
|
if owner is None:
|
|
return
|
|
ports = owner.inputs if direction == "input" else owner.outputs
|
|
ports[:] = [port for port in ports if port.id != port_id]
|
|
self.interfaceChanged.emit()
|
|
self.documentReset.emit()
|
|
|
|
def _move_interface_port(self, owner_id: str, port_id: str, position: QPointF) -> None:
|
|
if self.document is None:
|
|
return
|
|
owner = self.document.find_component(owner_id)
|
|
if owner is None:
|
|
return
|
|
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
|
if port is not None:
|
|
port.x, port.y = position.x(), position.y()
|
|
self.interfaceChanged.emit()
|
|
self.documentReset.emit()
|
|
|
|
def _rename_interface_port(self, owner_id: str, port_id: str, name: str) -> None:
|
|
owner = self.document.find_component(owner_id) if self.document else None
|
|
if owner is None:
|
|
return
|
|
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
|
if port is not None:
|
|
port.name = name
|
|
self.interfaceChanged.emit()
|
|
self.documentReset.emit()
|
|
|
|
def _rename_connection(self, owner_id: str, connection_id: str, name: str) -> None:
|
|
connection = self._graph_for(owner_id).connections.get(connection_id)
|
|
if connection is not None:
|
|
connection.name = name
|
|
self.documentReset.emit()
|
|
|
|
def _replace_source(self, component_id: str, source: dict) -> None:
|
|
if self.document is None:
|
|
return
|
|
component = self.document.find_component(component_id)
|
|
if component is None:
|
|
return
|
|
component.source = deepcopy(source)
|
|
self.documentReset.emit()
|
|
if component_id == self.active_component_id:
|
|
self.activeGraphChanged.emit()
|
|
|
|
def _set_component_appearance(self, component_id: str, values: dict) -> None:
|
|
if self.document is None:
|
|
return
|
|
component = self.document.find_component(component_id)
|
|
if component is None:
|
|
return
|
|
component.name = values["name"]
|
|
component.icon = Icon.from_dict(values["icon"])
|
|
component.inputs = [Port.from_dict(port) for port in values["inputs"]]
|
|
component.outputs = [Port.from_dict(port) for port in values["outputs"]]
|
|
component.show_subtree_in_library = values["show_subtree"]
|
|
component.properties = deepcopy(values["properties"])
|
|
self.documentReset.emit()
|
|
if component_id == self.active_component_id:
|
|
self.activeGraphChanged.emit()
|
|
|
|
def _set_component_properties(self, component_id: str, properties: dict) -> None:
|
|
if self.document is None:
|
|
return
|
|
component = self.document.find_component(component_id)
|
|
if component is not None:
|
|
component.properties = deepcopy(properties)
|
|
self.componentPropertiesChanged.emit(component_id)
|
|
|
|
def _set_component_parameters(self, component_id: str, values: list[dict]) -> None:
|
|
component = self.document.find_component(component_id) if self.document else None
|
|
if component is not None:
|
|
component.parameters = [Parameter.from_dict(item) for item in values]
|
|
self.documentReset.emit()
|
|
if (
|
|
component_id == self.active_component_id
|
|
and component.implementation_kind == "text"
|
|
):
|
|
self.textDefinitionChanged.emit(component_id)
|
|
|
|
def _delete_items(
|
|
self,
|
|
owner_id: str | None,
|
|
block_ids: set[str],
|
|
connection_ids: set[str],
|
|
input_ids: set[str],
|
|
output_ids: set[str],
|
|
) -> None:
|
|
if self.document is None:
|
|
return
|
|
deleted_component_ids: set[str] = set()
|
|
for block_id in block_ids:
|
|
component = self.document.find_component(block_id)
|
|
if component is not None:
|
|
deleted_component_ids.update(
|
|
child.id for child in self._component_subtree(component)
|
|
)
|
|
active_was_deleted = self.active_component_id in deleted_component_ids
|
|
if owner_id is None:
|
|
for block_id in block_ids:
|
|
self.document.roots.pop(block_id, None)
|
|
else:
|
|
owner = self.document.find_component(owner_id)
|
|
if owner is None:
|
|
return
|
|
for block_id in block_ids:
|
|
owner.graph.blocks.pop(block_id, None)
|
|
for connection_id in connection_ids:
|
|
owner.graph.connections.pop(connection_id, None)
|
|
owner.inputs[:] = [port for port in owner.inputs if port.id not in input_ids]
|
|
owner.outputs[:] = [port for port in owner.outputs if port.id not in output_ids]
|
|
if active_was_deleted:
|
|
self.active_component_id = owner_id or next(iter(self.document.roots), None)
|
|
self.activeGraphChanged.emit()
|
|
self.documentReset.emit()
|
|
|
|
def _restore_items(
|
|
self,
|
|
owner_id: str | None,
|
|
blocks: dict[str, Component],
|
|
connections: dict[str, Connection],
|
|
inputs: list[Port],
|
|
outputs: list[Port],
|
|
) -> None:
|
|
if self.document is None:
|
|
return
|
|
if owner_id is None:
|
|
self.document.roots.update(blocks)
|
|
else:
|
|
owner = self.document.find_component(owner_id)
|
|
if owner is None:
|
|
return
|
|
owner.graph.blocks.update(blocks)
|
|
owner.graph.connections.update(connections)
|
|
existing_inputs = {port.id for port in owner.inputs}
|
|
existing_outputs = {port.id for port in owner.outputs}
|
|
owner.inputs.extend(port for port in inputs if port.id not in existing_inputs)
|
|
owner.outputs.extend(port for port in outputs if port.id not in existing_outputs)
|
|
self.documentReset.emit()
|
|
|
|
@staticmethod
|
|
def _component_subtree(component: Component):
|
|
yield component
|
|
for child in component.graph.blocks.values():
|
|
yield from DocumentController._component_subtree(child)
|
|
|
|
def _set_text_definition(self, component_id: str, values: dict) -> None:
|
|
if self.document is None:
|
|
return
|
|
component = self.document.find_component(component_id)
|
|
if component is None:
|
|
return
|
|
component.inputs = [Port.from_dict(item) for item in values["inputs"]]
|
|
component.outputs = [Port.from_dict(item) for item in values["outputs"]]
|
|
component.source = deepcopy(values["source"])
|
|
component.parameters = [
|
|
Parameter.from_dict(item) for item in values.get("parameters", [])
|
|
]
|
|
self.interfaceChanged.emit()
|
|
self.documentReset.emit()
|
|
self.textDefinitionChanged.emit(component_id)
|