Better drawing and added more icons

This commit is contained in:
2026-07-20 13:18:16 +02:00
parent ee859c4311
commit 4ab74f64e2
27 changed files with 4087 additions and 307 deletions

View File

@@ -1,7 +1,7 @@
from PySide6.QtCore import QPointF
from PySide6.QtGui import QUndoCommand
from bedit.core.model import Component, Connection, Port
from bedit.core.model import Annotation, Component, Connection, Port
class AddComponentCommand(QUndoCommand):
@@ -76,6 +76,55 @@ class AddConnectionCommand(QUndoCommand):
self.controller._remove_connection(self.owner_id, self.connection.id)
class AddAnnotationCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, annotation: Annotation) -> None:
super().__init__(f"Draw {annotation.kind}")
self.controller, self.owner_id, self.annotation = controller, owner_id, annotation
def redo(self) -> None:
self.controller._insert_annotation(self.owner_id, self.annotation)
def undo(self) -> None:
self.controller._remove_annotation(self.owner_id, self.annotation.id)
class EditGraphItemCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str,
item_kind: str,
item_id: str,
old: dict,
new: dict,
text: str,
) -> None:
super().__init__(text)
self.controller, self.owner_id = controller, owner_id
self.item_kind, self.item_id = item_kind, item_id
self.old, self.new = old, new
def redo(self) -> None:
self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.new)
def undo(self) -> None:
self.controller._set_graph_item_data(self.owner_id, self.item_kind, self.item_id, self.old)
class DeleteAnnotationsCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, annotations: dict[str, Annotation]) -> None:
super().__init__("Delete annotations")
self.controller, self.owner_id, self.annotations = controller, owner_id, annotations
def redo(self) -> None:
for annotation_id in self.annotations:
self.controller._remove_annotation(self.owner_id, annotation_id)
def undo(self) -> None:
for annotation in self.annotations.values():
self.controller._insert_annotation(self.owner_id, annotation)
class ReplaceComponentCommand(QUndoCommand):
def __init__(self, controller, old: Component, new: Component) -> None:
super().__init__("Apply JSON changes")

View File

@@ -8,8 +8,11 @@ from PySide6.QtGui import QUndoStack
from bedit.gui.controllers.commands import (
AddComponentCommand,
AddConnectionCommand,
AddAnnotationCommand,
AddInterfacePortCommand,
DeleteSelectionCommand,
DeleteAnnotationsCommand,
EditGraphItemCommand,
EditTextDefinitionCommand,
EditComponentAppearanceCommand,
MoveComponentCommand,
@@ -21,6 +24,7 @@ from bedit.gui.controllers.commands import (
RotateComponentsCommand,
)
from bedit.core.model import (
Annotation,
Component,
Connection,
Endpoint,
@@ -43,6 +47,9 @@ class DocumentController(QObject):
componentRotated = Signal(str, float)
connectionAdded = Signal(str)
connectionRemoved = Signal(str)
graphItemChanged = Signal(str, str)
annotationAdded = Signal(str)
annotationRemoved = Signal(str)
interfaceChanged = Signal()
filePathChanged = Signal(object)
modifiedChanged = Signal(bool)
@@ -221,16 +228,14 @@ class DocumentController(QObject):
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)
)
self.undo_stack.push(RotateComponentsCommand(self, self.active_component_id, rotations))
def connect(
self,
source: Endpoint,
target: Endpoint,
*,
routing: str = "spline",
routing: str = "angled",
waypoints: list[QPointF] | None = None,
) -> str:
if self.active_component_id is None:
@@ -240,9 +245,7 @@ class DocumentController(QObject):
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}"
)
raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}")
if routing not in {"direct", "angled", "spline"}:
raise ValueError(f"Unknown connection routing: {routing}")
connection = Connection(
@@ -251,16 +254,154 @@ class DocumentController(QObject):
target,
properties={
"routing": routing,
"waypoints": [
{"x": point.x(), "y": point.y()} for point in (waypoints or [])
],
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
},
)
self.undo_stack.push(
AddConnectionCommand(self, self.active_component_id, connection)
)
self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
return connection.id
def add_annotation(
self,
kind: str,
start: QPointF,
end: QPointF,
*,
text: str = "",
routing: str = "angled",
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,
"routing": routing,
"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 set_route_waypoints(
self, item_kind: str, item_id: str, waypoints: list[QPointF], routing: str | None = None
) -> 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)
if routing is not None:
new["routing"] = routing
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:
@@ -272,7 +413,9 @@ class DocumentController(QObject):
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)
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")
@@ -289,9 +432,7 @@ class DocumentController(QObject):
x=position.x(),
y=position.y(),
)
self.undo_stack.push(
AddInterfacePortCommand(self, component.id, direction, port)
)
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:
@@ -350,11 +491,17 @@ class DocumentController(QObject):
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:
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:
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"
)
@@ -422,9 +569,15 @@ class DocumentController(QObject):
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:
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:
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:
@@ -471,7 +624,9 @@ class DocumentController(QObject):
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}
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
@@ -526,9 +681,7 @@ class DocumentController(QObject):
)
connections[connection.id] = connection
if blocks:
self.undo_stack.push(
PasteSelectionCommand(self, owner.id, blocks, connections)
)
self.undo_stack.push(PasteSelectionCommand(self, owner.id, blocks, connections))
return list(blocks)
def _graph_for(self, owner_id: str):
@@ -571,9 +724,7 @@ class DocumentController(QObject):
self.componentMoved.emit(component_id, position)
self.documentReset.emit()
def _rotate_component(
self, owner_id: str, component_id: str, rotation: float
) -> None:
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
@@ -593,6 +744,46 @@ class DocumentController(QObject):
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 == "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 _replace_component(self, old_id: str, replacement: Component) -> None:
if self.document is None:
return