Compare commits
2 Commits
60a30d4ee3
...
16d9cc7651
| Author | SHA1 | Date | |
|---|---|---|---|
| 16d9cc7651 | |||
| 1d535c1f5b |
BIN
examples/BondGraphs.beb
Normal file
BIN
examples/BondGraphs.beb
Normal file
Binary file not shown.
79
icons/signal_cosine_source.json
Normal file
79
icons/signal_cosine_source.json
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"shapes": {
|
||||||
|
"9ed4189a-123b-431f-a6bc-bbf169487703": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "rectangle",
|
||||||
|
"pos": [-48, -48],
|
||||||
|
"width": 96.0,
|
||||||
|
"height": 96.0,
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 5.0,
|
||||||
|
"corner_radius": 8.0,
|
||||||
|
"line_color": "#00007fff",
|
||||||
|
"fill_color": "#ebebebff"
|
||||||
|
},
|
||||||
|
"ac1b7cf2-ce50-44b7-9f8f-077ca36f7369": {
|
||||||
|
"layer": 4,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [-32, -32],
|
||||||
|
"end": [-32, 32],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 3.0,
|
||||||
|
"line_color": "#000000ff"
|
||||||
|
},
|
||||||
|
"2e623060-349e-4a48-97b0-a81921df5870": {
|
||||||
|
"layer": 2,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [-32, 32],
|
||||||
|
"end": [32, 32],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 3.0,
|
||||||
|
"line_color": "#000000ff"
|
||||||
|
},
|
||||||
|
"2c787197-3228-4f20-a57d-6fca410151b9": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-28, -16], "end": [-24, -15], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"53e49b2b-f3d6-4aa5-bd56-7ca7fbf68bb4": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-24, -15], "end": [-20, -11], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"7df5a7ce-b914-459e-93f6-f43450623baf": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-20, -11], "end": [-16, -7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"dd89bdfb-f10f-467a-aa90-a534e1f30dd8": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-16, -7], "end": [-12, 0], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"df9439f1-588f-46d8-873e-ecc59f5b8e36": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-12, 0], "end": [-8, 7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"4984ed23-a303-494f-947d-a34964220298": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-8, 7], "end": [-4, 13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"af40c4f7-d33b-426e-b130-ce78184a350f": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-4, 13], "end": [0, 16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"63c0188e-4f24-4ec7-b8d6-6258ab13c226": {
|
||||||
|
"layer": 5, "type": "line", "pos": [0, 16], "end": [4, 13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"c71d5e3b-a917-4dc8-8411-2046a54e2134": {
|
||||||
|
"layer": 5, "type": "line", "pos": [4, 13], "end": [8, 7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"51e128e6-c63f-4437-b56f-d20c31d195e2": {
|
||||||
|
"layer": 5, "type": "line", "pos": [8, 7], "end": [12, 0], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"f135f861-3e98-4bb1-ab43-e51fa0eec8db": {
|
||||||
|
"layer": 5, "type": "line", "pos": [12, 0], "end": [16, -7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"422ce3eb-8d0f-4b05-bb54-126583521240": {
|
||||||
|
"layer": 5, "type": "line", "pos": [16, -7], "end": [20, -11], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"c2c60fc4-fd4c-4cfc-a90a-dd729fe551b0": {
|
||||||
|
"layer": 5, "type": "line", "pos": [20, -11], "end": [24, -15], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"0852263b-237a-4a32-a56d-91945864e01b": {
|
||||||
|
"layer": 5, "type": "line", "pos": [24, -15], "end": [28, -16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"37eb6e52-6e89-4e09-8923-43c6230f5486": [-8, -8]
|
||||||
|
}
|
||||||
|
}
|
||||||
97
icons/signal_sine_source.json
Normal file
97
icons/signal_sine_source.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"shapes": {
|
||||||
|
"9ed4189a-123b-431f-a6bc-bbf169487703": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "rectangle",
|
||||||
|
"pos": [
|
||||||
|
-48,
|
||||||
|
-48
|
||||||
|
],
|
||||||
|
"width": 96.0,
|
||||||
|
"height": 96.0,
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 5.0,
|
||||||
|
"corner_radius": 8.0,
|
||||||
|
"line_color": "#00007fff",
|
||||||
|
"fill_color": "#ebebebff"
|
||||||
|
},
|
||||||
|
"ac1b7cf2-ce50-44b7-9f8f-077ca36f7369": {
|
||||||
|
"layer": 4,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
-32
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
-32,
|
||||||
|
32
|
||||||
|
],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 3.0,
|
||||||
|
"line_color": "#000000ff"
|
||||||
|
},
|
||||||
|
"2e623060-349e-4a48-97b0-a81921df5870": {
|
||||||
|
"layer": 2,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
32
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
32,
|
||||||
|
32
|
||||||
|
],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 3.0,
|
||||||
|
"line_color": "#000000ff"
|
||||||
|
},
|
||||||
|
"5cfcb42c-5cc3-48aa-ad92-7d19807c95a0": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-28, 0], "end": [-24, -7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"78f194ed-dae0-470a-a354-f7c4c3e54c81": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-24, -7], "end": [-20, -13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"cfa29bc9-5fac-4f00-bb02-52498fd14f92": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-20, -13], "end": [-16, -16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"8f852157-0395-4028-a9ce-f7bd596f2976": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-16, -16], "end": [-12, -16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"6f535277-a28e-4abc-82d4-4cf515a80357": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-12, -16], "end": [-8, -13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"a40bba3a-d9fd-4a7c-b409-2222ce69c558": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-8, -13], "end": [-4, -7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"2720ddd9-0332-46b6-bb79-dd90aa0892bd": {
|
||||||
|
"layer": 5, "type": "line", "pos": [-4, -7], "end": [0, 0], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"e420ce57-f79d-4941-8f5a-8207e15f0306": {
|
||||||
|
"layer": 5, "type": "line", "pos": [0, 0], "end": [4, 7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"f53f0310-7e14-4a87-aeb3-e079e2a8460e": {
|
||||||
|
"layer": 5, "type": "line", "pos": [4, 7], "end": [8, 13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"915cce34-b991-4119-88b5-c2dd41eec73c": {
|
||||||
|
"layer": 5, "type": "line", "pos": [8, 13], "end": [12, 16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"56d62c0f-4156-49eb-b048-b3b42304d6b0": {
|
||||||
|
"layer": 5, "type": "line", "pos": [12, 16], "end": [16, 16], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"de990624-dbb2-434c-bbdf-b0ea9caa34bd": {
|
||||||
|
"layer": 5, "type": "line", "pos": [16, 16], "end": [20, 13], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"b48fb633-b679-433f-8bee-f7c9549235aa": {
|
||||||
|
"layer": 5, "type": "line", "pos": [20, 13], "end": [24, 7], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"17f88457-c04d-4c08-90f6-f6ec5166c11c": {
|
||||||
|
"layer": 5, "type": "line", "pos": [24, 7], "end": [28, 0], "line_type": "solid", "line_thickness": 2.0, "line_color": "#ffaa00ff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"37eb6e52-6e89-4e09-8923-43c6230f5486": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
BIN
lib/signal.beb
BIN
lib/signal.beb
Binary file not shown.
Binary file not shown.
@@ -1,44 +1,52 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
from PySide6.QtGui import QUndoCommand
|
from PySide6.QtGui import QUndoCommand
|
||||||
|
|
||||||
from bedit_core.models import Component, ComponentID, Connection, ConnectionID, EquationImplementation, Graph, GraphImplementation, Interface
|
from bedit_core.models import Component, ComponentID, Connection, ConnectionID, EquationImplementation, Graph, GraphImplementation, Interface, PortID
|
||||||
from bedit_gui.models import Icon
|
from bedit_gui.models import Icon, PortMetadata
|
||||||
|
|
||||||
|
|
||||||
class AddEmptyGraphComponent(QUndoCommand):
|
class AddEmptyGraphComponent(QUndoCommand):
|
||||||
def __init__(self, document: object, parent: Component) -> None:
|
def __init__(self, document: object, parent: Component | dict[ComponentID, Component]) -> None:
|
||||||
super().__init__("Add graph component")
|
super().__init__("Add graph component")
|
||||||
|
self.document = document
|
||||||
|
if isinstance(parent, Component):
|
||||||
if not isinstance(parent.implementation, GraphImplementation):
|
if not isinstance(parent.implementation, GraphImplementation):
|
||||||
raise TypeError("parent component must have a graph implementation")
|
raise TypeError("parent component must have a graph implementation")
|
||||||
self.document = document
|
self.components = parent.implementation.graph.components
|
||||||
self.graph = parent.implementation.graph
|
else:
|
||||||
|
self.components = parent
|
||||||
self.component_id = ComponentID()
|
self.component_id = ComponentID()
|
||||||
self.component = Component(name="New Graph Component", interface=Interface(), parameters={}, implementation=GraphImplementation(Graph()))
|
self.component = Component(name="new_graph_component", interface=Interface(), parameters={}, implementation=GraphImplementation(Graph()))
|
||||||
|
|
||||||
def redo(self) -> None:
|
def redo(self) -> None:
|
||||||
self.graph.components[self.component_id] = self.component
|
self.components[self.component_id] = self.component
|
||||||
self.document.model_changed.emit(self.document.model)
|
self.document.model_changed.emit(self.document.model)
|
||||||
|
|
||||||
def undo(self) -> None:
|
def undo(self) -> None:
|
||||||
del self.graph.components[self.component_id]
|
del self.components[self.component_id]
|
||||||
self.document.model_changed.emit(self.document.model)
|
self.document.model_changed.emit(self.document.model)
|
||||||
|
|
||||||
|
|
||||||
class AddEmptyEquationComponent(QUndoCommand):
|
class AddEmptyEquationComponent(QUndoCommand):
|
||||||
def __init__(self, document: object, parent: Component) -> None:
|
def __init__(self, document: object, parent: Component | dict[ComponentID, Component]) -> None:
|
||||||
super().__init__("Add equation component")
|
super().__init__("Add equation component")
|
||||||
|
self.document = document
|
||||||
|
if isinstance(parent, Component):
|
||||||
if not isinstance(parent.implementation, GraphImplementation):
|
if not isinstance(parent.implementation, GraphImplementation):
|
||||||
raise TypeError("parent component must have a graph implementation")
|
raise TypeError("parent component must have a graph implementation")
|
||||||
self.document = document
|
self.components = parent.implementation.graph.components
|
||||||
self.graph = parent.implementation.graph
|
else:
|
||||||
|
self.components = parent
|
||||||
self.component_id = ComponentID()
|
self.component_id = ComponentID()
|
||||||
self.component = Component(name="New Equation Component", interface=Interface(), parameters={}, implementation=EquationImplementation())
|
self.component = Component(name="New Equation Component", interface=Interface(), parameters={}, implementation=EquationImplementation())
|
||||||
|
|
||||||
def redo(self) -> None:
|
def redo(self) -> None:
|
||||||
self.graph.components[self.component_id] = self.component
|
self.components[self.component_id] = self.component
|
||||||
self.document.model_changed.emit(self.document.model)
|
self.document.model_changed.emit(self.document.model)
|
||||||
|
|
||||||
def undo(self) -> None:
|
def undo(self) -> None:
|
||||||
del self.graph.components[self.component_id]
|
del self.components[self.component_id]
|
||||||
self.document.model_changed.emit(self.document.model)
|
self.document.model_changed.emit(self.document.model)
|
||||||
|
|
||||||
|
|
||||||
@@ -56,10 +64,16 @@ class DeleteComponent(QUndoCommand):
|
|||||||
if connection.source in port_ids or connection.target in port_ids:
|
if connection.source in port_ids or connection.target in port_ids:
|
||||||
self.connections.append((index, connection_id, connection))
|
self.connections.append((index, connection_id, connection))
|
||||||
self.icons = {}
|
self.icons = {}
|
||||||
|
self.port_metadata = {}
|
||||||
for component_id in self._component_ids(self.component_id, component):
|
for component_id in self._component_ids(self.component_id, component):
|
||||||
icon = document.stored_component_icon(component_id)
|
icon = document.stored_component_icon(component_id)
|
||||||
if icon is not None:
|
if icon is not None:
|
||||||
self.icons[component_id] = icon
|
self.icons[component_id] = icon
|
||||||
|
database = document._port_metadata_database(False)
|
||||||
|
if database is not None:
|
||||||
|
for port_id in self._port_ids(component):
|
||||||
|
if port_id in database.ports:
|
||||||
|
self.port_metadata[port_id] = deepcopy(database.ports[port_id])
|
||||||
|
|
||||||
def redo(self) -> None:
|
def redo(self) -> None:
|
||||||
if self.parent_graph is not None:
|
if self.parent_graph is not None:
|
||||||
@@ -69,6 +83,11 @@ class DeleteComponent(QUndoCommand):
|
|||||||
self.document.model_changed.emit(self.document.model)
|
self.document.model_changed.emit(self.document.model)
|
||||||
for component_id in self.icons:
|
for component_id in self.icons:
|
||||||
self.document._set_component_icon(component_id, None)
|
self.document._set_component_icon(component_id, None)
|
||||||
|
if self.port_metadata:
|
||||||
|
database = self.document.port_metadata_database()
|
||||||
|
for port_id in self.port_metadata:
|
||||||
|
database.ports.pop(port_id, None)
|
||||||
|
self.document._set_port_metadata_database(database)
|
||||||
|
|
||||||
def undo(self) -> None:
|
def undo(self) -> None:
|
||||||
self._restore_item(self.components, self.component_id, self.component, self.component_index)
|
self._restore_item(self.components, self.component_id, self.component, self.component_index)
|
||||||
@@ -78,6 +97,10 @@ class DeleteComponent(QUndoCommand):
|
|||||||
self.document.model_changed.emit(self.document.model)
|
self.document.model_changed.emit(self.document.model)
|
||||||
for component_id, icon in self.icons.items():
|
for component_id, icon in self.icons.items():
|
||||||
self.document._set_component_icon(component_id, icon)
|
self.document._set_component_icon(component_id, icon)
|
||||||
|
if self.port_metadata:
|
||||||
|
database = self.document.port_metadata_database()
|
||||||
|
database.ports.update(deepcopy(self.port_metadata))
|
||||||
|
self.document._set_port_metadata_database(database)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _find_component(cls, components: dict[ComponentID, Component], target: Component, parent_graph: Graph | None = None) -> tuple[dict[ComponentID, Component], ComponentID, Graph | None]:
|
def _find_component(cls, components: dict[ComponentID, Component], target: Component, parent_graph: Graph | None = None) -> tuple[dict[ComponentID, Component], ComponentID, Graph | None]:
|
||||||
@@ -99,6 +122,14 @@ class DeleteComponent(QUndoCommand):
|
|||||||
component_ids.extend(cls._component_ids(child_id, child))
|
component_ids.extend(cls._component_ids(child_id, child))
|
||||||
return component_ids
|
return component_ids
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _port_ids(cls, component: Component) -> list[PortID]:
|
||||||
|
port_ids = list(component.interface.ports)
|
||||||
|
if isinstance(component.implementation, GraphImplementation):
|
||||||
|
for child in component.implementation.graph.components.values():
|
||||||
|
port_ids.extend(cls._port_ids(child))
|
||||||
|
return port_ids
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _restore_item(items: dict, item_id: object, item: object, index: int) -> None:
|
def _restore_item(items: dict, item_id: object, item: object, index: int) -> None:
|
||||||
values = list(items.items())
|
values = list(items.items())
|
||||||
@@ -108,7 +139,7 @@ class DeleteComponent(QUndoCommand):
|
|||||||
|
|
||||||
|
|
||||||
class PasteComponents(QUndoCommand):
|
class PasteComponents(QUndoCommand):
|
||||||
def __init__(self, document: object, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], graph_id: ComponentID | None = None, positions: dict[ComponentID, tuple[int, int]] | None = None) -> None:
|
def __init__(self, document: object, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], graph_id: ComponentID | None = None, positions: dict[ComponentID, tuple[int, int]] | None = None, port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||||
super().__init__("Paste components")
|
super().__init__("Paste components")
|
||||||
self.document = document
|
self.document = document
|
||||||
self.target = target
|
self.target = target
|
||||||
@@ -116,6 +147,7 @@ class PasteComponents(QUndoCommand):
|
|||||||
self.icons = icons
|
self.icons = icons
|
||||||
self.graph_id = graph_id
|
self.graph_id = graph_id
|
||||||
self.positions = positions or {}
|
self.positions = positions or {}
|
||||||
|
self.port_metadata = deepcopy(port_metadata or {})
|
||||||
|
|
||||||
def redo(self) -> None:
|
def redo(self) -> None:
|
||||||
self.target.update(self.components)
|
self.target.update(self.components)
|
||||||
@@ -125,6 +157,10 @@ class PasteComponents(QUndoCommand):
|
|||||||
self.document.model_changed.emit(self.document.model)
|
self.document.model_changed.emit(self.document.model)
|
||||||
for component_id, icon in self.icons.items():
|
for component_id, icon in self.icons.items():
|
||||||
self.document._set_component_icon(component_id, icon)
|
self.document._set_component_icon(component_id, icon)
|
||||||
|
if self.port_metadata:
|
||||||
|
database = self.document.port_metadata_database()
|
||||||
|
database.ports.update(deepcopy(self.port_metadata))
|
||||||
|
self.document._set_port_metadata_database(database)
|
||||||
|
|
||||||
def undo(self) -> None:
|
def undo(self) -> None:
|
||||||
for component_id in self.components:
|
for component_id in self.components:
|
||||||
@@ -135,3 +171,8 @@ class PasteComponents(QUndoCommand):
|
|||||||
self.document.model_changed.emit(self.document.model)
|
self.document.model_changed.emit(self.document.model)
|
||||||
for component_id in self.icons:
|
for component_id in self.icons:
|
||||||
self.document._set_component_icon(component_id, None)
|
self.document._set_component_icon(component_id, None)
|
||||||
|
if self.port_metadata:
|
||||||
|
database = self.document.port_metadata_database()
|
||||||
|
for port_id in self.port_metadata:
|
||||||
|
database.ports.pop(port_id, None)
|
||||||
|
self.document._set_port_metadata_database(database)
|
||||||
|
|||||||
19
src/bedit_gui/commands/port_metadata_command.py
Normal file
19
src/bedit_gui/commands/port_metadata_command.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from PySide6.QtGui import QUndoCommand
|
||||||
|
|
||||||
|
from bedit_gui.models import PortMetadataDatabase
|
||||||
|
|
||||||
|
|
||||||
|
class ChangePortMetadataDatabaseCommand(QUndoCommand):
|
||||||
|
def __init__(self, document: object, database: PortMetadataDatabase) -> None:
|
||||||
|
super().__init__("Change port metadata")
|
||||||
|
self.document = document
|
||||||
|
self.old_database = document.stored_port_metadata_database()
|
||||||
|
self.new_database = deepcopy(database)
|
||||||
|
|
||||||
|
def redo(self) -> None:
|
||||||
|
self.document._set_port_metadata_database(self.new_database)
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
self.document._set_port_metadata_database(self.old_database)
|
||||||
@@ -133,11 +133,11 @@ class DocumentTreeClipboardHandler(ClipboardHandler):
|
|||||||
if target is None or payload is None:
|
if target is None or payload is None:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
components, icons = import_components(payload)
|
components, icons, port_metadata = import_components(payload)
|
||||||
except (TypeError, ValueError) as exc:
|
except (TypeError, ValueError) as exc:
|
||||||
QMessageBox.critical(self.tree, "Could not paste components", str(exc))
|
QMessageBox.critical(self.tree, "Could not paste components", str(exc))
|
||||||
return
|
return
|
||||||
self.document.paste_components(target, components, icons)
|
self.document.paste_components(target, components, icons, port_metadata)
|
||||||
|
|
||||||
def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
self.document.delete_components(self._selected_components())
|
self.document.delete_components(self._selected_components())
|
||||||
@@ -223,14 +223,14 @@ class GraphEditorClipboardHandler(ClipboardHandler):
|
|||||||
|
|
||||||
def _paste_payload(self, graph_component: Component, payload: dict, position: tuple[int, int]) -> None:
|
def _paste_payload(self, graph_component: Component, payload: dict, position: tuple[int, int]) -> None:
|
||||||
try:
|
try:
|
||||||
components, icons = import_components(payload)
|
components, icons, port_metadata = import_components(payload)
|
||||||
except (TypeError, ValueError) as exc:
|
except (TypeError, ValueError) as exc:
|
||||||
QMessageBox.critical(self.editor, "Could not paste components", str(exc))
|
QMessageBox.critical(self.editor, "Could not paste components", str(exc))
|
||||||
return
|
return
|
||||||
x, y = position
|
x, y = position
|
||||||
spacing = self.editor.snap_to_grid_size * 4
|
spacing = self.editor.snap_to_grid_size * 4
|
||||||
positions = {component_id: (x + index * spacing, y + index * spacing) for index, component_id in enumerate(components)}
|
positions = {component_id: (x + index * spacing, y + index * spacing) for index, component_id in enumerate(components)}
|
||||||
self.document.paste_graph_components(graph_component, components, icons, positions)
|
self.document.paste_graph_components(graph_component, components, icons, positions, port_metadata)
|
||||||
|
|
||||||
def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
components = self._selected_components()
|
components = self._selected_components()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from PySide6.QtWidgets import QAbstractItemView, QDialog, QHeaderView, QMenu
|
|||||||
from bedit_core.models import Component, ComponentID, ConnectionID, 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, GraphComponentLabel, Icon
|
from bedit_gui.models import Graph, GraphComponentLabel, Icon, PortMetadata
|
||||||
from bedit_gui.views.dialogs.interface_editor_dialog import InterfaceEditorDialog
|
from bedit_gui.views.dialogs.interface_editor_dialog import InterfaceEditorDialog
|
||||||
from bedit_gui.views.dialogs.param_editor_dialog import ParamEditorDialog
|
from bedit_gui.views.dialogs.param_editor_dialog import ParamEditorDialog
|
||||||
from bedit_gui.views.icon_editor_window import IconEditorWindow
|
from bedit_gui.views.icon_editor_window import IconEditorWindow
|
||||||
@@ -17,11 +17,12 @@ from bedit_gui.views.main_window import MainWindow
|
|||||||
from bedit_gui.views.models.document_tree_model import DocumentTreeModel
|
from bedit_gui.views.models.document_tree_model import DocumentTreeModel
|
||||||
from bedit_gui.utils.icon import render_fitted_icon
|
from bedit_gui.utils.icon import render_fitted_icon
|
||||||
|
|
||||||
ICON_SIZE = QSize(32, 32)
|
ICON_SIZE = QSize(16, 16)
|
||||||
|
|
||||||
class InterfaceEditorLike(Protocol):
|
class InterfaceEditorLike(Protocol):
|
||||||
def exec(self) -> int: ...
|
def exec(self) -> int: ...
|
||||||
def ports(self) -> dict[PortID, Port]: ...
|
def ports(self) -> dict[PortID, Port]: ...
|
||||||
|
def port_metadata(self) -> dict[PortID, PortMetadata]: ...
|
||||||
|
|
||||||
class ParamEditorLike(Protocol):
|
class ParamEditorLike(Protocol):
|
||||||
def exec(self) -> int: ...
|
def exec(self) -> int: ...
|
||||||
@@ -29,7 +30,7 @@ class ParamEditorLike(Protocol):
|
|||||||
|
|
||||||
|
|
||||||
InterfaceEditorFactory = Callable[
|
InterfaceEditorFactory = Callable[
|
||||||
[dict[PortID, Port], MainWindow],
|
[dict[PortID, Port], dict[PortID, PortMetadata], MainWindow],
|
||||||
InterfaceEditorLike,
|
InterfaceEditorLike,
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -60,15 +61,17 @@ class DocumentTreeController(QObject):
|
|||||||
window.ui.documentTree.setModel(self.model)
|
window.ui.documentTree.setModel(self.model)
|
||||||
window.ui.documentTree.selectionModel().selectionChanged.connect(self._selection_changed)
|
window.ui.documentTree.selectionModel().selectionChanged.connect(self._selection_changed)
|
||||||
window.equation_editor.equation_text_change_requested.connect(self.document.update_component_equation_text)
|
window.equation_editor.equation_text_change_requested.connect(self.document.update_component_equation_text)
|
||||||
|
window.equation_editor.port_metadata_change_requested.connect(self._change_equation_port_metadata)
|
||||||
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.port_metadata_database_changed.connect(self._on_port_metadata_database_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_component_label_changed.connect(self._on_graph_component_label_changed)
|
document.graph_component_label_changed.connect(self._on_graph_component_label_changed)
|
||||||
document.graph_connection_points_changed.connect(self._on_graph_connection_points_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_moves_requested.connect(self.document.move_graph_components)
|
||||||
window.graph_editor.component_label_move_requested.connect(self.document.move_graph_component_label)
|
window.graph_editor.component_label_move_requested.connect(self.document.move_graph_component_label)
|
||||||
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.component_open_requested.connect(self._open_graph_component)
|
window.graph_editor.component_open_requested.connect(self._open_graph_component)
|
||||||
@@ -84,11 +87,11 @@ class DocumentTreeController(QObject):
|
|||||||
window.ui.documentTree.setHeaderHidden(True)
|
window.ui.documentTree.setHeaderHidden(True)
|
||||||
window.ui.documentTree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
window.ui.documentTree.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||||
window.ui.documentTree.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
window.ui.documentTree.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
window.ui.documentTree.setIconSize(QSize(48, 48))
|
window.ui.documentTree.setIconSize(QSize(24, 24))
|
||||||
window.ui.documentTree.header().setStretchLastSection(False)
|
window.ui.documentTree.header().setStretchLastSection(False)
|
||||||
window.ui.documentTree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
window.ui.documentTree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||||
window.ui.documentTree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
window.ui.documentTree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
window.ui.documentTree.setColumnWidth(1, 56)
|
window.ui.documentTree.setColumnWidth(1, 28)
|
||||||
self._tree_viewport = window.ui.documentTree.viewport()
|
self._tree_viewport = window.ui.documentTree.viewport()
|
||||||
self._tree_viewport.installEventFilter(self)
|
self._tree_viewport.installEventFilter(self)
|
||||||
|
|
||||||
@@ -125,7 +128,8 @@ class DocumentTreeController(QObject):
|
|||||||
|
|
||||||
def _show_component(self, component: Component | None) -> None:
|
def _show_component(self, component: Component | None) -> None:
|
||||||
if component is not None and isinstance(component.implementation, EquationImplementation):
|
if component is not None and isinstance(component.implementation, EquationImplementation):
|
||||||
self.window.equation_editor.set_component(component)
|
port_metadata = {port_id: self.document.port_metadata(port_id) for port_id in component.interface.ports}
|
||||||
|
self.window.equation_editor.set_component(component, port_metadata)
|
||||||
self.window.equation_editor.show()
|
self.window.equation_editor.show()
|
||||||
else:
|
else:
|
||||||
self.window.equation_editor.set_component(None)
|
self.window.equation_editor.set_component(None)
|
||||||
@@ -133,7 +137,8 @@ class DocumentTreeController(QObject):
|
|||||||
if component is not None and isinstance(component.implementation, GraphImplementation):
|
if component is not None and isinstance(component.implementation, GraphImplementation):
|
||||||
graph = self.document.graph_database().graphs.get(self.document.component_id(component), Graph())
|
graph = self.document.graph_database().graphs.get(self.document.component_id(component), Graph())
|
||||||
icons = {component_id: self.document.component_icon(component_id) for component_id in component.implementation.graph.components}
|
icons = {component_id: self.document.component_icon(component_id) for component_id in component.implementation.graph.components}
|
||||||
self.window.graph_editor.set_component(component, graph, icons)
|
port_metadata = {port_id: self.document.port_metadata(port_id) for child in component.implementation.graph.components.values() for port_id in child.interface.ports}
|
||||||
|
self.window.graph_editor.set_component(component, graph, icons, port_metadata)
|
||||||
self.window.graph_editor.show()
|
self.window.graph_editor.show()
|
||||||
else:
|
else:
|
||||||
self.window.graph_editor.set_component(None)
|
self.window.graph_editor.set_component(None)
|
||||||
@@ -152,6 +157,17 @@ class DocumentTreeController(QObject):
|
|||||||
if graph_component is not None and isinstance(graph_component.implementation, GraphImplementation) and component_id in graph_component.implementation.graph.components:
|
if graph_component is not None and isinstance(graph_component.implementation, GraphImplementation) and component_id in graph_component.implementation.graph.components:
|
||||||
self._show_component(graph_component)
|
self._show_component(graph_component)
|
||||||
|
|
||||||
|
def _on_port_metadata_database_changed(self, _database: object) -> None:
|
||||||
|
graph_component = self.window.graph_editor.component()
|
||||||
|
if graph_component is not None:
|
||||||
|
self._show_component(graph_component)
|
||||||
|
equation_component = self.window.equation_editor.component()
|
||||||
|
if equation_component is not None:
|
||||||
|
self.window.equation_editor.refresh_port_metadata({port_id: self.document.port_metadata(port_id) for port_id in equation_component.interface.ports})
|
||||||
|
|
||||||
|
def _change_equation_port_metadata(self, component: Component, port_metadata: dict[PortID, PortMetadata]) -> None:
|
||||||
|
self.document.update_component_ports(component, component.interface.ports, port_metadata)
|
||||||
|
|
||||||
def _on_graph_component_position_changed(self, graph_id: ComponentID, component_id: ComponentID, position: tuple[int, int] | None) -> None:
|
def _on_graph_component_position_changed(self, graph_id: ComponentID, component_id: ComponentID, position: tuple[int, int] | None) -> None:
|
||||||
graph_component = self.window.graph_editor.component()
|
graph_component = self.window.graph_editor.component()
|
||||||
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:
|
||||||
@@ -175,11 +191,22 @@ class DocumentTreeController(QObject):
|
|||||||
|
|
||||||
def _show_context_menu(self, position: QPoint) -> None:
|
def _show_context_menu(self, position: QPoint) -> None:
|
||||||
index = self.window.ui.documentTree.indexAt(position)
|
index = self.window.ui.documentTree.indexAt(position)
|
||||||
component = self.model.value(index)
|
value = self.model.value(index)
|
||||||
if not isinstance(component, Component):
|
global_position = self.window.ui.documentTree.viewport().mapToGlobal(position)
|
||||||
return
|
if isinstance(value, CoreDocument):
|
||||||
|
self._show_root_context_menu(global_position)
|
||||||
|
elif isinstance(value, Component):
|
||||||
|
self._show_component_context_menu(value, global_position)
|
||||||
|
|
||||||
self._show_component_context_menu(component, self.window.ui.documentTree.viewport().mapToGlobal(position))
|
def _show_root_context_menu(self, global_position: QPoint) -> None:
|
||||||
|
menu = QMenu(self.window.ui.documentTree)
|
||||||
|
add_graph_component = menu.addAction("Add Graph Component")
|
||||||
|
add_equation_component = menu.addAction("Add Equation Component")
|
||||||
|
selected = menu.exec(global_position)
|
||||||
|
if selected is add_graph_component:
|
||||||
|
self.document.add_empty_root_graph_component()
|
||||||
|
elif selected is add_equation_component:
|
||||||
|
self.document.add_empty_root_equation_component()
|
||||||
|
|
||||||
def _show_graph_component_context_menu(self, component_id: ComponentID, global_position: QPoint) -> None:
|
def _show_graph_component_context_menu(self, component_id: ComponentID, global_position: QPoint) -> None:
|
||||||
component = self._components.get(component_id)
|
component = self._components.get(component_id)
|
||||||
@@ -231,10 +258,11 @@ class DocumentTreeController(QObject):
|
|||||||
def _edit_interface(self, component: Component) -> None:
|
def _edit_interface(self, component: Component) -> None:
|
||||||
dialog = self.interface_editor_factory(
|
dialog = self.interface_editor_factory(
|
||||||
component.interface.ports,
|
component.interface.ports,
|
||||||
|
{port_id: self.document.port_metadata(port_id) for port_id in component.interface.ports},
|
||||||
self.window,
|
self.window,
|
||||||
)
|
)
|
||||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
self.document.update_component_ports(component, dialog.ports())
|
self.document.update_component_ports(component, dialog.ports(), dialog.port_metadata())
|
||||||
|
|
||||||
def _edit_params(self, component: Component) -> None:
|
def _edit_params(self, component: Component) -> None:
|
||||||
dialog = self.param_editor_factory(
|
dialog = self.param_editor_factory(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from PySide6.QtCore import QObject, QSize, Qt
|
from PySide6.QtCore import QObject, QSize, Qt
|
||||||
from PySide6.QtWidgets import QAbstractItemView, QHeaderView
|
from PySide6.QtWidgets import QAbstractItemView, QHeaderView
|
||||||
|
|
||||||
from bedit_core.models import Component, ComponentID, GraphImplementation
|
from bedit_core.models import Component, ComponentID, GraphImplementation, PortID
|
||||||
from bedit_gui.models import Icon, IconDatabase
|
from bedit_gui.models import Icon, IconDatabase, PortMetadata, PortMetadataDatabase
|
||||||
from bedit_gui.services.application_settings import ApplicationSettings
|
from bedit_gui.services.application_settings import ApplicationSettings
|
||||||
from bedit_gui.services.component_clipboard import export_component_data
|
from bedit_gui.services.component_clipboard import export_component_data
|
||||||
from bedit_gui.services.libraries import load_library_documents
|
from bedit_gui.services.libraries import load_library_documents
|
||||||
@@ -18,7 +18,7 @@ class LibraryController(QObject):
|
|||||||
super().__init__(window)
|
super().__init__(window)
|
||||||
self.window = window
|
self.window = window
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self._component_sources: dict[int, tuple[ComponentID, dict[ComponentID, Icon]]] = {}
|
self._component_sources: dict[int, tuple[ComponentID, dict[ComponentID, Icon], dict[PortID, PortMetadata]]] = {}
|
||||||
self.model = LibraryTreeModel(self._component_payload)
|
self.model = LibraryTreeModel(self._component_payload)
|
||||||
|
|
||||||
tree = window.ui.libraryTree
|
tree = window.ui.libraryTree
|
||||||
@@ -44,7 +44,9 @@ class LibraryController(QObject):
|
|||||||
for library in libraries:
|
for library in libraries:
|
||||||
database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None
|
database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None
|
||||||
icons = database.icons if isinstance(database, IconDatabase) else {}
|
icons = database.icons if isinstance(database, IconDatabase) else {}
|
||||||
self._collect_component_sources(library.document.root, icons)
|
metadata_database = library.document.metadata.get("port_metadata_database") if library.document.metadata is not None else None
|
||||||
|
port_metadata = metadata_database.ports if isinstance(metadata_database, PortMetadataDatabase) else {}
|
||||||
|
self._collect_component_sources(library.document.root, icons, port_metadata)
|
||||||
self.model.set_documents([library.document for library in libraries])
|
self.model.set_documents([library.document for library in libraries])
|
||||||
for library in libraries:
|
for library in libraries:
|
||||||
database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None
|
database = library.document.metadata.get("icon_database") if library.document.metadata is not None else None
|
||||||
@@ -55,20 +57,22 @@ class LibraryController(QObject):
|
|||||||
def _component_payload(self, components: list[Component]) -> dict:
|
def _component_payload(self, components: list[Component]) -> dict:
|
||||||
roots = {}
|
roots = {}
|
||||||
icons = {}
|
icons = {}
|
||||||
|
port_metadata = {}
|
||||||
for component in components:
|
for component in components:
|
||||||
source = self._component_sources.get(id(component))
|
source = self._component_sources.get(id(component))
|
||||||
if source is None:
|
if source is None:
|
||||||
continue
|
continue
|
||||||
component_id, source_icons = source
|
component_id, source_icons, source_port_metadata = source
|
||||||
roots[component_id] = component
|
roots[component_id] = component
|
||||||
icons.update(source_icons)
|
icons.update(source_icons)
|
||||||
return export_component_data(roots, icons)
|
port_metadata.update(source_port_metadata)
|
||||||
|
return export_component_data(roots, icons, port_metadata)
|
||||||
|
|
||||||
def _collect_component_sources(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
|
def _collect_component_sources(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], port_metadata: dict[PortID, PortMetadata]) -> None:
|
||||||
for component_id, component in components.items():
|
for component_id, component in components.items():
|
||||||
self._component_sources[id(component)] = (component_id, icons)
|
self._component_sources[id(component)] = (component_id, icons, port_metadata)
|
||||||
if isinstance(component.implementation, GraphImplementation):
|
if isinstance(component.implementation, GraphImplementation):
|
||||||
self._collect_component_sources(component.implementation.graph.components, icons)
|
self._collect_component_sources(component.implementation.graph.components, icons, port_metadata)
|
||||||
|
|
||||||
def _set_component_icons(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
|
def _set_component_icons(self, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
|
||||||
for component_id, component in components.items():
|
for component_id, component in components.items():
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PySide6.QtCore import QObject, Signal
|
from PySide6.QtCore import QObject, Signal
|
||||||
|
from PySide6.QtWidgets import QAbstractItemView
|
||||||
|
|
||||||
from bedit_gui.services.application_logging import configure_logging
|
from bedit_gui.services.application_logging import configure_logging
|
||||||
from bedit_gui.views.main_window import MainWindow
|
from bedit_gui.views.main_window import MainWindow
|
||||||
@@ -49,4 +50,6 @@ class LogController(QObject):
|
|||||||
self.emitter.message.connect(self.model.append)
|
self.emitter.message.connect(self.model.append)
|
||||||
self.model.rowsInserted.connect(window.ui.listView.scrollToBottom)
|
self.model.rowsInserted.connect(window.ui.listView.scrollToBottom)
|
||||||
window.ui.listView.setModel(self.model)
|
window.ui.listView.setModel(self.model)
|
||||||
|
window.ui.listView.setWordWrap(True)
|
||||||
|
window.ui.listView.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
||||||
configure_logging(self.handler, level)
|
configure_logging(self.handler, level)
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class SimulationController(QObject):
|
|||||||
self.document.infer_causality(component)
|
self.document.infer_causality(component)
|
||||||
build = self.compiler(component, build_directory)
|
build = self.compiler(component, build_directory)
|
||||||
except (OSError, RuntimeError, ValueError) as exc:
|
except (OSError, RuntimeError, ValueError) as exc:
|
||||||
logger.exception("Could not compile model %s", component_path)
|
logger.error("Could not compile model %s:\n%s", component_path, exc)
|
||||||
QMessageBox.critical(self.window, "Could not compile", str(exc))
|
QMessageBox.critical(self.window, "Could not compile", str(exc))
|
||||||
self.window.statusBar().showMessage("Compilation failed")
|
self.window.statusBar().showMessage("Compilation failed")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ from bedit_gui.commands.graph_connection_points_command import ChangeGraphConnec
|
|||||||
from bedit_gui.commands.graph_connection_command import AddGraphConnectionCommand, DeleteGraphConnectionCommand
|
from bedit_gui.commands.graph_connection_command import AddGraphConnectionCommand, DeleteGraphConnectionCommand
|
||||||
from bedit_gui.commands.graph_label_command import ChangeGraphComponentLabelCommand
|
from bedit_gui.commands.graph_label_command import ChangeGraphComponentLabelCommand
|
||||||
from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand
|
from bedit_gui.commands.port_commands import AddPortCommand, ChangePortCommand, RemovePortCommand
|
||||||
|
from bedit_gui.commands.port_metadata_command import ChangePortMetadataDatabaseCommand
|
||||||
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, GraphComponentLabel, GraphConnection, GraphDatabase, Icon, IconDatabase, Simulation, SimulationDatabase
|
from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, GraphDatabase, Icon, IconDatabase, PortMetadata, PortMetadataDatabase, Simulation, SimulationDatabase
|
||||||
from bedit_gui.services import document_files
|
from bedit_gui.services import document_files
|
||||||
|
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ class Document(QObject):
|
|||||||
icon_changed = Signal(object, object)
|
icon_changed = Signal(object, object)
|
||||||
equation_text_changed = Signal(object, str)
|
equation_text_changed = Signal(object, str)
|
||||||
simulation_database_changed = Signal(object)
|
simulation_database_changed = Signal(object)
|
||||||
|
port_metadata_database_changed = Signal(object)
|
||||||
graph_component_position_changed = Signal(object, object, object)
|
graph_component_position_changed = Signal(object, object, object)
|
||||||
graph_component_label_changed = Signal(object, object, object)
|
graph_component_label_changed = Signal(object, object, object)
|
||||||
graph_connection_points_changed = Signal(object, object, object)
|
graph_connection_points_changed = Signal(object, object, object)
|
||||||
@@ -140,6 +142,40 @@ class Document(QObject):
|
|||||||
database = self._graph_database(False)
|
database = self._graph_database(False)
|
||||||
return deepcopy(database) if database is not None else GraphDatabase()
|
return deepcopy(database) if database is not None else GraphDatabase()
|
||||||
|
|
||||||
|
def stored_port_metadata_database(self) -> PortMetadataDatabase | None:
|
||||||
|
database = self._port_metadata_database(False)
|
||||||
|
return deepcopy(database) if database is not None else None
|
||||||
|
|
||||||
|
def port_metadata_database(self) -> PortMetadataDatabase:
|
||||||
|
return self.stored_port_metadata_database() or PortMetadataDatabase()
|
||||||
|
|
||||||
|
def port_metadata(self, port_id: PortID) -> PortMetadata:
|
||||||
|
return deepcopy(self.port_metadata_database().ports.get(port_id, PortMetadata()))
|
||||||
|
|
||||||
|
def _set_port_metadata_database(self, database: PortMetadataDatabase | None) -> None:
|
||||||
|
if database is None or not database.ports:
|
||||||
|
if self.model.metadata is not None:
|
||||||
|
self.model.metadata.pop("port_metadata_database", None)
|
||||||
|
else:
|
||||||
|
if self.model.metadata is None:
|
||||||
|
self.model.metadata = {}
|
||||||
|
self.model.metadata["port_metadata_database"] = deepcopy(database)
|
||||||
|
self.port_metadata_database_changed.emit(self.stored_port_metadata_database())
|
||||||
|
|
||||||
|
def _port_metadata_database(self, create: bool) -> PortMetadataDatabase | None:
|
||||||
|
metadata = self.model.metadata
|
||||||
|
value = metadata.get("port_metadata_database") if metadata is not None else None
|
||||||
|
if isinstance(value, PortMetadataDatabase):
|
||||||
|
return value
|
||||||
|
if not create:
|
||||||
|
return None
|
||||||
|
if metadata is None:
|
||||||
|
metadata = {}
|
||||||
|
self.model.metadata = metadata
|
||||||
|
database = PortMetadataDatabase()
|
||||||
|
metadata["port_metadata_database"] = database
|
||||||
|
return database
|
||||||
|
|
||||||
def move_graph_component(self, graph_component: Component, component_id: ComponentID, position: tuple[int, int]) -> None:
|
def move_graph_component(self, graph_component: Component, component_id: ComponentID, position: tuple[int, int]) -> None:
|
||||||
graph_id = self.component_id(graph_component)
|
graph_id = self.component_id(graph_component)
|
||||||
database = self._graph_database(False)
|
database = self._graph_database(False)
|
||||||
@@ -147,6 +183,18 @@ class Document(QObject):
|
|||||||
if graph is None or graph.component_positions.get(component_id) != position:
|
if graph is None or graph.component_positions.get(component_id) != position:
|
||||||
self.undo_stack.push(MoveGraphComponentCommand(self, graph_id, component_id, position))
|
self.undo_stack.push(MoveGraphComponentCommand(self, graph_id, component_id, position))
|
||||||
|
|
||||||
|
def move_graph_components(self, graph_component: Component, positions: dict[ComponentID, tuple[int, int]]) -> None:
|
||||||
|
graph_id = self.component_id(graph_component)
|
||||||
|
database = self._graph_database(False)
|
||||||
|
graph = database.graphs.get(graph_id) if database is not None else None
|
||||||
|
changed = {component_id: position for component_id, position in positions.items() if graph is None or graph.component_positions.get(component_id) != position}
|
||||||
|
if not changed:
|
||||||
|
return
|
||||||
|
self.undo_stack.beginMacro("Move graph components" if len(changed) > 1 else "Move graph component")
|
||||||
|
for component_id, position in changed.items():
|
||||||
|
self.undo_stack.push(MoveGraphComponentCommand(self, graph_id, component_id, position))
|
||||||
|
self.undo_stack.endMacro()
|
||||||
|
|
||||||
def _set_graph_component_position(self, graph_id: ComponentID, component_id: ComponentID, position: tuple[int, int] | None) -> None:
|
def _set_graph_component_position(self, graph_id: ComponentID, component_id: ComponentID, position: tuple[int, int] | None) -> None:
|
||||||
if position is None:
|
if position is None:
|
||||||
database = self._graph_database(False)
|
database = self._graph_database(False)
|
||||||
@@ -309,7 +357,7 @@ class Document(QObject):
|
|||||||
metadata["icon_database"] = database
|
metadata["icon_database"] = database
|
||||||
return database
|
return database
|
||||||
|
|
||||||
def update_component_ports(self, component: Component, ports: dict[PortID, Port]) -> None:
|
def update_component_ports(self, component: Component, ports: dict[PortID, Port], port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||||
current = component.interface.ports
|
current = component.interface.ports
|
||||||
removed = [
|
removed = [
|
||||||
RemovePortCommand(self, component, port_id)
|
RemovePortCommand(self, component, port_id)
|
||||||
@@ -325,12 +373,23 @@ class Document(QObject):
|
|||||||
if current[port_id] != ports[port_id]
|
if current[port_id] != ports[port_id]
|
||||||
]
|
]
|
||||||
commands = [*removed, *added, *changed]
|
commands = [*removed, *added, *changed]
|
||||||
if not commands:
|
database = self.port_metadata_database()
|
||||||
|
component_port_ids = set(current) | set(ports)
|
||||||
|
for port_id in component_port_ids:
|
||||||
|
metadata = port_metadata.get(port_id, PortMetadata()) if port_metadata is not None and port_id in ports else PortMetadata()
|
||||||
|
if metadata == PortMetadata():
|
||||||
|
database.ports.pop(port_id, None)
|
||||||
|
else:
|
||||||
|
database.ports[port_id] = deepcopy(metadata)
|
||||||
|
metadata_changed = database != self.port_metadata_database()
|
||||||
|
if not commands and not metadata_changed:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.undo_stack.beginMacro("Edit interface")
|
self.undo_stack.beginMacro("Edit interface")
|
||||||
for command in commands:
|
for command in commands:
|
||||||
self.undo_stack.push(command)
|
self.undo_stack.push(command)
|
||||||
|
if metadata_changed:
|
||||||
|
self.undo_stack.push(ChangePortMetadataDatabaseCommand(self, database))
|
||||||
self.undo_stack.endMacro()
|
self.undo_stack.endMacro()
|
||||||
|
|
||||||
|
|
||||||
@@ -369,12 +428,22 @@ class Document(QObject):
|
|||||||
command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name)
|
command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name)
|
||||||
self.undo_stack.push(command)
|
self.undo_stack.push(command)
|
||||||
|
|
||||||
|
def add_empty_root_graph_component(self) -> None:
|
||||||
|
command = AddEmptyGraphComponent(self, self.model.root)
|
||||||
|
command.component.name = self._unique_component_name(self.model.root, command.component.name)
|
||||||
|
self.undo_stack.push(command)
|
||||||
|
|
||||||
def add_empty_equation_component(self, component: Component) -> None:
|
def add_empty_equation_component(self, component: Component) -> None:
|
||||||
if isinstance(component.implementation, GraphImplementation):
|
if isinstance(component.implementation, GraphImplementation):
|
||||||
command = AddEmptyEquationComponent(self, component)
|
command = AddEmptyEquationComponent(self, component)
|
||||||
command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name)
|
command.component.name = self._unique_component_name(component.implementation.graph.components, command.component.name)
|
||||||
self.undo_stack.push(command)
|
self.undo_stack.push(command)
|
||||||
|
|
||||||
|
def add_empty_root_equation_component(self) -> None:
|
||||||
|
command = AddEmptyEquationComponent(self, self.model.root)
|
||||||
|
command.component.name = self._unique_component_name(self.model.root, command.component.name)
|
||||||
|
self.undo_stack.push(command)
|
||||||
|
|
||||||
def delete_component(self, component: Component) -> None:
|
def delete_component(self, component: Component) -> None:
|
||||||
self.undo_stack.push(DeleteComponent(self, component))
|
self.undo_stack.push(DeleteComponent(self, component))
|
||||||
|
|
||||||
@@ -386,16 +455,16 @@ class Document(QObject):
|
|||||||
self.undo_stack.push(DeleteComponent(self, component))
|
self.undo_stack.push(DeleteComponent(self, component))
|
||||||
self.undo_stack.endMacro()
|
self.undo_stack.endMacro()
|
||||||
|
|
||||||
def paste_components(self, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> None:
|
def paste_components(self, target: dict[ComponentID, Component], components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||||
if not components:
|
if not components:
|
||||||
return
|
return
|
||||||
names = {component.name for component in target.values()}
|
names = {component.name for component in target.values()}
|
||||||
for component in components.values():
|
for component in components.values():
|
||||||
component.name = self._unique_name(names, component.name)
|
component.name = self._unique_name(names, component.name)
|
||||||
names.add(component.name)
|
names.add(component.name)
|
||||||
self.undo_stack.push(PasteComponents(self, target, components, icons))
|
self.undo_stack.push(PasteComponents(self, target, components, icons, port_metadata=port_metadata))
|
||||||
|
|
||||||
def paste_graph_components(self, graph_component: Component, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], positions: dict[ComponentID, tuple[int, int]]) -> None:
|
def paste_graph_components(self, graph_component: Component, components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], positions: dict[ComponentID, tuple[int, int]], port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||||
if not isinstance(graph_component.implementation, GraphImplementation) or not components:
|
if not isinstance(graph_component.implementation, GraphImplementation) or not components:
|
||||||
return
|
return
|
||||||
target = graph_component.implementation.graph.components
|
target = graph_component.implementation.graph.components
|
||||||
@@ -403,7 +472,7 @@ class Document(QObject):
|
|||||||
for component in components.values():
|
for component in components.values():
|
||||||
component.name = self._unique_name(names, component.name)
|
component.name = self._unique_name(names, component.name)
|
||||||
names.add(component.name)
|
names.add(component.name)
|
||||||
self.undo_stack.push(PasteComponents(self, target, components, icons, self.component_id(graph_component), positions))
|
self.undo_stack.push(PasteComponents(self, target, components, icons, self.component_id(graph_component), positions, port_metadata))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _unique_component_name(components: dict[ComponentID, Component], name: str) -> str:
|
def _unique_component_name(components: dict[ComponentID, Component], name: str) -> str:
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class Shape:
|
|||||||
def from_data(cls, data: Mapping[str, Any]) -> Shape:
|
def from_data(cls, data: Mapping[str, Any]) -> Shape:
|
||||||
if cls is Shape and data.get("type") == "rectangle":
|
if cls is Shape and data.get("type") == "rectangle":
|
||||||
return Rectangle.from_data(data)
|
return Rectangle.from_data(data)
|
||||||
|
if cls is Shape and data.get("type") == "ellipse":
|
||||||
|
return Ellipse.from_data(data)
|
||||||
if cls is Shape and data.get("type") == "text":
|
if cls is Shape and data.get("type") == "text":
|
||||||
return Text.from_data(data)
|
return Text.from_data(data)
|
||||||
if cls is Shape and data.get("type") == "line":
|
if cls is Shape and data.get("type") == "line":
|
||||||
@@ -77,6 +79,24 @@ class Rectangle(Shape):
|
|||||||
def to_data(self) -> dict[str, Any]:
|
def to_data(self) -> dict[str, Any]:
|
||||||
return {**super().to_data(), "width": self.width, "height": self.height, "line_type": self.line_type.value, "line_thickness": self.line_thickness, "corner_radius": self.corner_radius, "line_color": self.line_color, "fill_color": self.fill_color}
|
return {**super().to_data(), "width": self.width, "height": self.height, "line_type": self.line_type.value, "line_thickness": self.line_thickness, "corner_radius": self.corner_radius, "line_color": self.line_color, "fill_color": self.fill_color}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Ellipse(Shape):
|
||||||
|
type: str = field(init=False, default="ellipse")
|
||||||
|
width: float = 32.0
|
||||||
|
height: float = 32.0
|
||||||
|
line_type: LineType = LineType.SOLID
|
||||||
|
line_thickness: float = 1.0
|
||||||
|
line_color: str = "#000000ff"
|
||||||
|
fill_color: str = "#ffffff00"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data: Mapping[str, Any]) -> Ellipse:
|
||||||
|
pos = data.get("pos", [0, 0])
|
||||||
|
return cls(layer=int(data.get("layer", 0)), pos=(int(pos[0]), int(pos[1])), width=float(data.get("width", 100.0)), height=float(data.get("height", 100.0)), line_type=LineType(data.get("line_type", "solid")), line_thickness=float(data.get("line_thickness", 1.0)), line_color=str(data.get("line_color", "#000000ff")), fill_color=str(data.get("fill_color", "#ffffff00")))
|
||||||
|
|
||||||
|
def to_data(self) -> dict[str, Any]:
|
||||||
|
return {**super().to_data(), "width": self.width, "height": self.height, "line_type": self.line_type.value, "line_thickness": self.line_thickness, "line_color": self.line_color, "fill_color": self.fill_color}
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Text(Shape):
|
class Text(Shape):
|
||||||
type: str = field(init=False, default="text")
|
type: str = field(init=False, default="text")
|
||||||
@@ -123,6 +143,31 @@ class IconDatabase:
|
|||||||
def to_data(self) -> dict[str, Any]:
|
def to_data(self) -> dict[str, Any]:
|
||||||
return {"format_version": self.format_version, "icons": {str(key): icon.to_data() for key, icon in self.icons.items()}}
|
return {"format_version": self.format_version, "icons": {str(key): icon.to_data() for key, icon in self.icons.items()}}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PortMetadata:
|
||||||
|
connection_annotation: str | None = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data: Mapping[str, Any]) -> PortMetadata:
|
||||||
|
annotation = data.get("connection_annotation")
|
||||||
|
return cls(connection_annotation=str(annotation) if annotation else None)
|
||||||
|
|
||||||
|
def to_data(self) -> dict[str, Any]:
|
||||||
|
return {"connection_annotation": self.connection_annotation}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PortMetadataDatabase:
|
||||||
|
format_version: int = 1
|
||||||
|
ports: dict[PortID, PortMetadata] = field(default_factory=dict)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data: Mapping[str, Any]) -> PortMetadataDatabase:
|
||||||
|
ports = {PortID(key): PortMetadata.from_data(value) for key, value in data.get("ports", {}).items()}
|
||||||
|
return cls(format_version=int(data.get("format_version", 1)), ports=ports)
|
||||||
|
|
||||||
|
def to_data(self) -> dict[str, Any]:
|
||||||
|
return {"format_version": self.format_version, "ports": {str(key): metadata.to_data() for key, metadata in self.ports.items()}}
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GraphConnection:
|
class GraphConnection:
|
||||||
points: list[tuple[int, int]] = field(default_factory=list)
|
points: list[tuple[int, int]] = field(default_factory=list)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Any
|
|||||||
from bedit_core.models import Component, ComponentID, ConnectionID, Document, GraphImplementation, ID, ParameterID, PortID
|
from bedit_core.models import Component, ComponentID, ConnectionID, Document, GraphImplementation, ID, ParameterID, PortID
|
||||||
from bedit_core.serialization.schema import document_from_data, document_to_data
|
from bedit_core.serialization.schema import document_from_data, document_to_data
|
||||||
from bedit_gui.documents import Document as GuiDocument
|
from bedit_gui.documents import Document as GuiDocument
|
||||||
from bedit_gui.models import Icon, ShapeID
|
from bedit_gui.models import Icon, PortMetadata, ShapeID
|
||||||
|
|
||||||
FORMAT_VERSION = 1
|
FORMAT_VERSION = 1
|
||||||
COMPONENTS_MIME = "application/x-bedit-components+json"
|
COMPONENTS_MIME = "application/x-bedit-components+json"
|
||||||
@@ -16,21 +16,27 @@ def export_components(document: GuiDocument, components: list[Component]) -> dic
|
|||||||
roots = {document.component_id(component): component for component in components}
|
roots = {document.component_id(component): component for component in components}
|
||||||
component_ids = _all_component_ids(roots)
|
component_ids = _all_component_ids(roots)
|
||||||
icons = {}
|
icons = {}
|
||||||
|
port_metadata = {}
|
||||||
for component_id in component_ids:
|
for component_id in component_ids:
|
||||||
icon = document.stored_component_icon(component_id)
|
icon = document.stored_component_icon(component_id)
|
||||||
if icon is not None:
|
if icon is not None:
|
||||||
icons[component_id] = icon
|
icons[component_id] = icon
|
||||||
return export_component_data(roots, icons)
|
database = document.port_metadata_database()
|
||||||
|
for port_id in _all_port_ids(roots):
|
||||||
|
if port_id in database.ports:
|
||||||
|
port_metadata[port_id] = database.ports[port_id]
|
||||||
|
return export_component_data(roots, icons, port_metadata)
|
||||||
|
|
||||||
|
|
||||||
def export_component_data(components: dict[ComponentID, Component], icons: dict[ComponentID, Icon]) -> dict[str, Any]:
|
def export_component_data(components: dict[ComponentID, Component], icons: dict[ComponentID, Icon], port_metadata: dict[PortID, PortMetadata] | None = None) -> dict[str, Any]:
|
||||||
serialized = document_to_data(Document(format_version=1, id=ID(), name="Clipboard", root=components))
|
serialized = document_to_data(Document(format_version=1, id=ID(), name="Clipboard", root=components))
|
||||||
component_ids = set(_all_component_ids(components))
|
component_ids = set(_all_component_ids(components))
|
||||||
icon_data = {str(component_id): icon.to_data() for component_id, icon in icons.items() if component_id in component_ids}
|
icon_data = {str(component_id): icon.to_data() for component_id, icon in icons.items() if component_id in component_ids}
|
||||||
return {"format_version": FORMAT_VERSION, "type": "components", "components": serialized["root"], "icons": icon_data}
|
metadata_data = {str(port_id): metadata.to_data() for port_id, metadata in (port_metadata or {}).items() if port_id in set(_all_port_ids(components))}
|
||||||
|
return {"format_version": FORMAT_VERSION, "type": "components", "components": serialized["root"], "icons": icon_data, "port_metadata": metadata_data}
|
||||||
|
|
||||||
|
|
||||||
def import_components(payload: dict[str, Any]) -> tuple[dict[ComponentID, Component], dict[ComponentID, Icon]]:
|
def import_components(payload: dict[str, Any]) -> tuple[dict[ComponentID, Component], dict[ComponentID, Icon], dict[PortID, PortMetadata]]:
|
||||||
if payload.get("format_version") != FORMAT_VERSION or payload.get("type") != "components":
|
if payload.get("format_version") != FORMAT_VERSION or payload.get("type") != "components":
|
||||||
raise ValueError("unsupported component clipboard format")
|
raise ValueError("unsupported component clipboard format")
|
||||||
components = payload.get("components")
|
components = payload.get("components")
|
||||||
@@ -52,7 +58,11 @@ def import_components(payload: dict[str, Any]) -> tuple[dict[ComponentID, Compon
|
|||||||
icon.shapes = {ShapeID(): shape for shape in icon.shapes.values()}
|
icon.shapes = {ShapeID(): shape for shape in icon.shapes.values()}
|
||||||
icon.port_positions = {port_map[port_id]: position for port_id, position in icon.port_positions.items() if port_id in port_map}
|
icon.port_positions = {port_map[port_id]: position for port_id, position in icon.port_positions.items() if port_id in port_map}
|
||||||
icons[new_component_id] = icon
|
icons[new_component_id] = icon
|
||||||
return remapped, icons
|
metadata_data = payload.get("port_metadata", {})
|
||||||
|
if not isinstance(metadata_data, dict):
|
||||||
|
raise TypeError("component clipboard port metadata must be an object")
|
||||||
|
port_metadata = {port_map[PortID(old_id)]: PortMetadata.from_data(data) for old_id, data in metadata_data.items() if PortID(old_id) in port_map and isinstance(data, dict)}
|
||||||
|
return remapped, icons, port_metadata
|
||||||
|
|
||||||
|
|
||||||
def _remap_components(components: dict[ComponentID, Component], component_map: dict[ComponentID, ComponentID], port_map: dict[PortID, PortID]) -> dict[ComponentID, Component]:
|
def _remap_components(components: dict[ComponentID, Component], component_map: dict[ComponentID, ComponentID], port_map: dict[PortID, PortID]) -> dict[ComponentID, Component]:
|
||||||
@@ -87,3 +97,12 @@ def _all_component_ids(components: dict[ComponentID, Component]) -> list[Compone
|
|||||||
if isinstance(component.implementation, GraphImplementation):
|
if isinstance(component.implementation, GraphImplementation):
|
||||||
component_ids.extend(_all_component_ids(component.implementation.graph.components))
|
component_ids.extend(_all_component_ids(component.implementation.graph.components))
|
||||||
return component_ids
|
return component_ids
|
||||||
|
|
||||||
|
|
||||||
|
def _all_port_ids(components: dict[ComponentID, Component]) -> list[PortID]:
|
||||||
|
port_ids: list[PortID] = []
|
||||||
|
for component in components.values():
|
||||||
|
port_ids.extend(component.interface.ports)
|
||||||
|
if isinstance(component.implementation, GraphImplementation):
|
||||||
|
port_ids.extend(_all_port_ids(component.implementation.graph.components))
|
||||||
|
return port_ids
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
from bedit_core.models import Document
|
from bedit_core.models import Document
|
||||||
from bedit_core.serialization import load as load_document
|
from bedit_core.serialization import load as load_document
|
||||||
from bedit_core.serialization import save as save_document
|
from bedit_core.serialization import save as save_document
|
||||||
from bedit_gui.models import GraphDatabase, IconDatabase, SimulationDatabase
|
from bedit_gui.models import GraphDatabase, IconDatabase, PortMetadataDatabase, SimulationDatabase
|
||||||
|
|
||||||
|
|
||||||
def load(path: str | Path) -> Document:
|
def load(path: str | Path) -> Document:
|
||||||
@@ -18,6 +18,8 @@ def load(path: str | Path) -> Document:
|
|||||||
document.metadata["graph_database"] = GraphDatabase.from_data(document.metadata["graph_database"])
|
document.metadata["graph_database"] = GraphDatabase.from_data(document.metadata["graph_database"])
|
||||||
if document.metadata is not None and isinstance(document.metadata.get("simulation_database"), dict):
|
if document.metadata is not None and isinstance(document.metadata.get("simulation_database"), dict):
|
||||||
document.metadata["simulation_database"] = SimulationDatabase.from_data(document.metadata["simulation_database"])
|
document.metadata["simulation_database"] = SimulationDatabase.from_data(document.metadata["simulation_database"])
|
||||||
|
if document.metadata is not None and isinstance(document.metadata.get("port_metadata_database"), dict):
|
||||||
|
document.metadata["port_metadata_database"] = PortMetadataDatabase.from_data(document.metadata["port_metadata_database"])
|
||||||
return document
|
return document
|
||||||
|
|
||||||
|
|
||||||
@@ -30,4 +32,6 @@ def save(document: Document, path: str | Path) -> None:
|
|||||||
saved_document.metadata["graph_database"] = saved_document.metadata["graph_database"].to_data()
|
saved_document.metadata["graph_database"] = saved_document.metadata["graph_database"].to_data()
|
||||||
if saved_document.metadata is not None and isinstance(saved_document.metadata.get("simulation_database"), SimulationDatabase):
|
if saved_document.metadata is not None and isinstance(saved_document.metadata.get("simulation_database"), SimulationDatabase):
|
||||||
saved_document.metadata["simulation_database"] = saved_document.metadata["simulation_database"].to_data()
|
saved_document.metadata["simulation_database"] = saved_document.metadata["simulation_database"].to_data()
|
||||||
|
if saved_document.metadata is not None and isinstance(saved_document.metadata.get("port_metadata_database"), PortMetadataDatabase):
|
||||||
|
saved_document.metadata["port_metadata_database"] = saved_document.metadata["port_metadata_database"].to_data()
|
||||||
save_document(saved_document, path)
|
save_document(saved_document, path)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>800</width>
|
<width>800</width>
|
||||||
<height>22</height>
|
<height>19</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QMenu" name="menuEdit">
|
<widget class="QMenu" name="menuEdit">
|
||||||
@@ -80,6 +80,7 @@
|
|||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</attribute>
|
</attribute>
|
||||||
<addaction name="actionAdd_Rectangle"/>
|
<addaction name="actionAdd_Rectangle"/>
|
||||||
|
<addaction name="actionAdd_Circle"/>
|
||||||
<addaction name="actionAdd_Text"/>
|
<addaction name="actionAdd_Text"/>
|
||||||
<addaction name="actionAdd_Line"/>
|
<addaction name="actionAdd_Line"/>
|
||||||
</widget>
|
</widget>
|
||||||
@@ -236,6 +237,21 @@
|
|||||||
<enum>QAction::MenuRole::NoRole</enum>
|
<enum>QAction::MenuRole::NoRole</enum>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="actionAdd_Circle">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../resources/resources.qrc">
|
||||||
|
<normaloff>:/icons/icons/draw-circle.png</normaloff>:/icons/icons/draw-circle.png</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Add Ellipse</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Add a circle</string>
|
||||||
|
</property>
|
||||||
|
<property name="menuRole">
|
||||||
|
<enum>QAction::MenuRole::NoRole</enum>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../resources/resources.qrc"/>
|
<include location="../../resources/resources.qrc"/>
|
||||||
|
|||||||
206
src/bedit_gui/ui/forms/icon_editor_window_ui.py
Normal file
206
src/bedit_gui/ui/forms/icon_editor_window_ui.py
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## Form generated from reading UI file 'icon_editor_window.ui'
|
||||||
|
##
|
||||||
|
## Created by: Qt User Interface Compiler version 6.11.1
|
||||||
|
##
|
||||||
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||||
|
QMetaObject, QObject, QPoint, QRect,
|
||||||
|
QSize, QTime, QUrl, Qt)
|
||||||
|
from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
|
||||||
|
QCursor, QFont, QFontDatabase, QGradient,
|
||||||
|
QIcon, QImage, QKeySequence, QLinearGradient,
|
||||||
|
QPainter, QPalette, QPixmap, QRadialGradient,
|
||||||
|
QTransform)
|
||||||
|
from PySide6.QtWidgets import (QApplication, QGraphicsView, QMainWindow, QMenu,
|
||||||
|
QMenuBar, QSizePolicy, QStatusBar, QToolBar,
|
||||||
|
QVBoxLayout, QWidget)
|
||||||
|
import resources_rc
|
||||||
|
|
||||||
|
class Ui_iconEditor(object):
|
||||||
|
def setupUi(self, iconEditor):
|
||||||
|
if not iconEditor.objectName():
|
||||||
|
iconEditor.setObjectName(u"iconEditor")
|
||||||
|
iconEditor.resize(800, 600)
|
||||||
|
icon = QIcon()
|
||||||
|
icon.addFile(u":/icons/icons/draw-path.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
iconEditor.setWindowIcon(icon)
|
||||||
|
self.actionUndo = QAction(iconEditor)
|
||||||
|
self.actionUndo.setObjectName(u"actionUndo")
|
||||||
|
icon1 = QIcon()
|
||||||
|
icon1.addFile(u":/icons/icons/edit-undo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionUndo.setIcon(icon1)
|
||||||
|
self.actionUndo.setMenuRole(QAction.MenuRole.NoRole)
|
||||||
|
self.actionRedo = QAction(iconEditor)
|
||||||
|
self.actionRedo.setObjectName(u"actionRedo")
|
||||||
|
icon2 = QIcon()
|
||||||
|
icon2.addFile(u":/icons/icons/edit-redo.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionRedo.setIcon(icon2)
|
||||||
|
self.actionRedo.setMenuRole(QAction.MenuRole.NoRole)
|
||||||
|
self.actionSave = QAction(iconEditor)
|
||||||
|
self.actionSave.setObjectName(u"actionSave")
|
||||||
|
icon3 = QIcon()
|
||||||
|
icon3.addFile(u":/icons/icons/document-save.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionSave.setIcon(icon3)
|
||||||
|
self.actionSave.setMenuRole(QAction.MenuRole.NoRole)
|
||||||
|
self.actionCancel = QAction(iconEditor)
|
||||||
|
self.actionCancel.setObjectName(u"actionCancel")
|
||||||
|
icon4 = QIcon()
|
||||||
|
icon4.addFile(u":/icons/icons/dialog-close.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionCancel.setIcon(icon4)
|
||||||
|
self.actionCancel.setMenuRole(QAction.MenuRole.NoRole)
|
||||||
|
self.actionAdd_Rectangle = QAction(iconEditor)
|
||||||
|
self.actionAdd_Rectangle.setObjectName(u"actionAdd_Rectangle")
|
||||||
|
icon5 = QIcon()
|
||||||
|
icon5.addFile(u":/icons/icons/draw-rectangle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionAdd_Rectangle.setIcon(icon5)
|
||||||
|
self.actionAdd_Rectangle.setMenuRole(QAction.MenuRole.NoRole)
|
||||||
|
self.actionAdd_Text = QAction(iconEditor)
|
||||||
|
self.actionAdd_Text.setObjectName(u"actionAdd_Text")
|
||||||
|
icon6 = QIcon()
|
||||||
|
icon6.addFile(u":/icons/icons/draw-text.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionAdd_Text.setIcon(icon6)
|
||||||
|
self.actionAdd_Text.setMenuRole(QAction.MenuRole.NoRole)
|
||||||
|
self.actionSave_to_File = QAction(iconEditor)
|
||||||
|
self.actionSave_to_File.setObjectName(u"actionSave_to_File")
|
||||||
|
icon7 = QIcon()
|
||||||
|
icon7.addFile(u":/icons/icons/document-save-as.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionSave_to_File.setIcon(icon7)
|
||||||
|
self.actionSave_to_File.setMenuRole(QAction.MenuRole.NoRole)
|
||||||
|
self.actionOpen_from_File = QAction(iconEditor)
|
||||||
|
self.actionOpen_from_File.setObjectName(u"actionOpen_from_File")
|
||||||
|
icon8 = QIcon()
|
||||||
|
icon8.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionOpen_from_File.setIcon(icon8)
|
||||||
|
self.actionOpen_from_File.setMenuRole(QAction.MenuRole.NoRole)
|
||||||
|
self.actionAdd_Line = QAction(iconEditor)
|
||||||
|
self.actionAdd_Line.setObjectName(u"actionAdd_Line")
|
||||||
|
self.actionAdd_Line.setIcon(icon)
|
||||||
|
self.actionAdd_Line.setMenuRole(QAction.MenuRole.NoRole)
|
||||||
|
self.actionAdd_Circle = QAction(iconEditor)
|
||||||
|
self.actionAdd_Circle.setObjectName(u"actionAdd_Circle")
|
||||||
|
icon9 = QIcon()
|
||||||
|
icon9.addFile(u":/icons/icons/draw-circle.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||||
|
self.actionAdd_Circle.setIcon(icon9)
|
||||||
|
self.actionAdd_Circle.setMenuRole(QAction.MenuRole.NoRole)
|
||||||
|
self.centralwidget = QWidget(iconEditor)
|
||||||
|
self.centralwidget.setObjectName(u"centralwidget")
|
||||||
|
self.verticalLayout = QVBoxLayout(self.centralwidget)
|
||||||
|
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||||
|
self.graphicsView = QGraphicsView(self.centralwidget)
|
||||||
|
self.graphicsView.setObjectName(u"graphicsView")
|
||||||
|
|
||||||
|
self.verticalLayout.addWidget(self.graphicsView)
|
||||||
|
|
||||||
|
iconEditor.setCentralWidget(self.centralwidget)
|
||||||
|
self.menubar = QMenuBar(iconEditor)
|
||||||
|
self.menubar.setObjectName(u"menubar")
|
||||||
|
self.menubar.setGeometry(QRect(0, 0, 800, 19))
|
||||||
|
self.menuEdit = QMenu(self.menubar)
|
||||||
|
self.menuEdit.setObjectName(u"menuEdit")
|
||||||
|
self.menuFile = QMenu(self.menubar)
|
||||||
|
self.menuFile.setObjectName(u"menuFile")
|
||||||
|
iconEditor.setMenuBar(self.menubar)
|
||||||
|
self.statusbar = QStatusBar(iconEditor)
|
||||||
|
self.statusbar.setObjectName(u"statusbar")
|
||||||
|
iconEditor.setStatusBar(self.statusbar)
|
||||||
|
self.actionToolbar = QToolBar(iconEditor)
|
||||||
|
self.actionToolbar.setObjectName(u"actionToolbar")
|
||||||
|
iconEditor.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.actionToolbar)
|
||||||
|
self.iconToolbar = QToolBar(iconEditor)
|
||||||
|
self.iconToolbar.setObjectName(u"iconToolbar")
|
||||||
|
iconEditor.addToolBar(Qt.ToolBarArea.TopToolBarArea, self.iconToolbar)
|
||||||
|
|
||||||
|
self.menubar.addAction(self.menuFile.menuAction())
|
||||||
|
self.menubar.addAction(self.menuEdit.menuAction())
|
||||||
|
self.menuEdit.addAction(self.actionUndo)
|
||||||
|
self.menuEdit.addAction(self.actionRedo)
|
||||||
|
self.menuFile.addAction(self.actionOpen_from_File)
|
||||||
|
self.menuFile.addAction(self.actionSave_to_File)
|
||||||
|
self.menuFile.addSeparator()
|
||||||
|
self.menuFile.addAction(self.actionSave)
|
||||||
|
self.menuFile.addAction(self.actionCancel)
|
||||||
|
self.actionToolbar.addAction(self.actionUndo)
|
||||||
|
self.actionToolbar.addAction(self.actionRedo)
|
||||||
|
self.actionToolbar.addAction(self.actionSave)
|
||||||
|
self.actionToolbar.addAction(self.actionCancel)
|
||||||
|
self.iconToolbar.addAction(self.actionAdd_Rectangle)
|
||||||
|
self.iconToolbar.addAction(self.actionAdd_Circle)
|
||||||
|
self.iconToolbar.addAction(self.actionAdd_Text)
|
||||||
|
self.iconToolbar.addAction(self.actionAdd_Line)
|
||||||
|
|
||||||
|
self.retranslateUi(iconEditor)
|
||||||
|
|
||||||
|
QMetaObject.connectSlotsByName(iconEditor)
|
||||||
|
# setupUi
|
||||||
|
|
||||||
|
def retranslateUi(self, iconEditor):
|
||||||
|
iconEditor.setWindowTitle(QCoreApplication.translate("iconEditor", u"MainWindow", None))
|
||||||
|
self.actionUndo.setText(QCoreApplication.translate("iconEditor", u"Undo", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.actionUndo.setToolTip(QCoreApplication.translate("iconEditor", u"Undo", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionUndo.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Z", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionRedo.setText(QCoreApplication.translate("iconEditor", u"Redo", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.actionRedo.setToolTip(QCoreApplication.translate("iconEditor", u"Redo", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionRedo.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Y", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionSave.setText(QCoreApplication.translate("iconEditor", u"Save", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.actionSave.setToolTip(QCoreApplication.translate("iconEditor", u"Save icon", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionSave.setShortcut(QCoreApplication.translate("iconEditor", u"Return", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionCancel.setText(QCoreApplication.translate("iconEditor", u"Cancel", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.actionCancel.setToolTip(QCoreApplication.translate("iconEditor", u"Cancel icon editing", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionCancel.setShortcut(QCoreApplication.translate("iconEditor", u"Shift+Esc", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionAdd_Rectangle.setText(QCoreApplication.translate("iconEditor", u"Add Rectangle", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.actionAdd_Rectangle.setToolTip(QCoreApplication.translate("iconEditor", u"Add a rectangle", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.actionAdd_Text.setText(QCoreApplication.translate("iconEditor", u"Add Text", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.actionAdd_Text.setToolTip(QCoreApplication.translate("iconEditor", u"Add a text field", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.actionSave_to_File.setText(QCoreApplication.translate("iconEditor", u"Save to File", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.actionSave_to_File.setToolTip(QCoreApplication.translate("iconEditor", u"Save icon to a file", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionSave_to_File.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Shift+S", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionOpen_from_File.setText(QCoreApplication.translate("iconEditor", u"Open from File", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.actionOpen_from_File.setToolTip(QCoreApplication.translate("iconEditor", u"Open icon from File", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
#if QT_CONFIG(shortcut)
|
||||||
|
self.actionOpen_from_File.setShortcut(QCoreApplication.translate("iconEditor", u"Ctrl+Shift+O", None))
|
||||||
|
#endif // QT_CONFIG(shortcut)
|
||||||
|
self.actionAdd_Line.setText(QCoreApplication.translate("iconEditor", u"Add Line", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.actionAdd_Line.setToolTip(QCoreApplication.translate("iconEditor", u"Add a line", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.actionAdd_Circle.setText(QCoreApplication.translate("iconEditor", u"Add Ellipse", None))
|
||||||
|
#if QT_CONFIG(tooltip)
|
||||||
|
self.actionAdd_Circle.setToolTip(QCoreApplication.translate("iconEditor", u"Add a circle", None))
|
||||||
|
#endif // QT_CONFIG(tooltip)
|
||||||
|
self.menuEdit.setTitle(QCoreApplication.translate("iconEditor", u"Edit", None))
|
||||||
|
self.menuFile.setTitle(QCoreApplication.translate("iconEditor", u"File", None))
|
||||||
|
self.actionToolbar.setWindowTitle(QCoreApplication.translate("iconEditor", u"toolBar", None))
|
||||||
|
self.iconToolbar.setWindowTitle(QCoreApplication.translate("iconEditor", u"toolBar", None))
|
||||||
|
# retranslateUi
|
||||||
|
|
||||||
@@ -148,6 +148,20 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="5" column="0">
|
||||||
|
<widget class="QLabel" name="connectionAnnotationLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Connection annotation:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="1">
|
||||||
|
<widget class="QLineEdit" name="connectionAnnotationEdit">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>For example, + or -</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
|||||||
268
src/bedit_gui/ui/forms/port_editor_widget_ui.py
Normal file
268
src/bedit_gui/ui/forms/port_editor_widget_ui.py
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
## Form generated from reading UI file 'port_editor_widget.ui'
|
||||||
|
##
|
||||||
|
## Created by: Qt User Interface Compiler version 6.11.1
|
||||||
|
##
|
||||||
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||||
|
QMetaObject, QObject, QPoint, QRect,
|
||||||
|
QSize, QTime, QUrl, Qt)
|
||||||
|
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||||
|
QFont, QFontDatabase, QGradient, QIcon,
|
||||||
|
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||||
|
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||||
|
from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QFormLayout,
|
||||||
|
QFrame, QHBoxLayout, QLabel, QLineEdit,
|
||||||
|
QListView, QPlainTextEdit, QPushButton, QRadioButton,
|
||||||
|
QSizePolicy, QSpacerItem, QSpinBox, QVBoxLayout,
|
||||||
|
QWidget)
|
||||||
|
|
||||||
|
class Ui_PortEditor(object):
|
||||||
|
def setupUi(self, PortEditor):
|
||||||
|
if not PortEditor.objectName():
|
||||||
|
PortEditor.setObjectName(u"PortEditor")
|
||||||
|
PortEditor.resize(541, 420)
|
||||||
|
self.horizontalLayout_2 = QHBoxLayout(PortEditor)
|
||||||
|
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
||||||
|
self.leftColumn = QVBoxLayout()
|
||||||
|
self.leftColumn.setObjectName(u"leftColumn")
|
||||||
|
self.portList = QListView(PortEditor)
|
||||||
|
self.portList.setObjectName(u"portList")
|
||||||
|
|
||||||
|
self.leftColumn.addWidget(self.portList)
|
||||||
|
|
||||||
|
self.buttonRow = QHBoxLayout()
|
||||||
|
self.buttonRow.setObjectName(u"buttonRow")
|
||||||
|
self.addPort = QPushButton(PortEditor)
|
||||||
|
self.addPort.setObjectName(u"addPort")
|
||||||
|
|
||||||
|
self.buttonRow.addWidget(self.addPort)
|
||||||
|
|
||||||
|
self.removePort = QPushButton(PortEditor)
|
||||||
|
self.removePort.setObjectName(u"removePort")
|
||||||
|
|
||||||
|
self.buttonRow.addWidget(self.removePort)
|
||||||
|
|
||||||
|
|
||||||
|
self.leftColumn.addLayout(self.buttonRow)
|
||||||
|
|
||||||
|
|
||||||
|
self.horizontalLayout_2.addLayout(self.leftColumn)
|
||||||
|
|
||||||
|
self.rightColumn = QVBoxLayout()
|
||||||
|
self.rightColumn.setObjectName(u"rightColumn")
|
||||||
|
self.basicForm = QFormLayout()
|
||||||
|
self.basicForm.setObjectName(u"basicForm")
|
||||||
|
self.nameLabel = QLabel(PortEditor)
|
||||||
|
self.nameLabel.setObjectName(u"nameLabel")
|
||||||
|
|
||||||
|
self.basicForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel)
|
||||||
|
|
||||||
|
self.nameEdit = QLineEdit(PortEditor)
|
||||||
|
self.nameEdit.setObjectName(u"nameEdit")
|
||||||
|
|
||||||
|
self.basicForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
|
||||||
|
|
||||||
|
self.typeLabel = QLabel(PortEditor)
|
||||||
|
self.typeLabel.setObjectName(u"typeLabel")
|
||||||
|
|
||||||
|
self.basicForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.typeLabel)
|
||||||
|
|
||||||
|
self.typeRow = QHBoxLayout()
|
||||||
|
self.typeRow.setObjectName(u"typeRow")
|
||||||
|
self.typeSignal = QRadioButton(PortEditor)
|
||||||
|
self.typeSignal.setObjectName(u"typeSignal")
|
||||||
|
|
||||||
|
self.typeRow.addWidget(self.typeSignal)
|
||||||
|
|
||||||
|
self.typeBond = QRadioButton(PortEditor)
|
||||||
|
self.typeBond.setObjectName(u"typeBond")
|
||||||
|
|
||||||
|
self.typeRow.addWidget(self.typeBond)
|
||||||
|
|
||||||
|
|
||||||
|
self.basicForm.setLayout(1, QFormLayout.ItemRole.FieldRole, self.typeRow)
|
||||||
|
|
||||||
|
self.orientationLabel = QLabel(PortEditor)
|
||||||
|
self.orientationLabel.setObjectName(u"orientationLabel")
|
||||||
|
|
||||||
|
self.basicForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.orientationLabel)
|
||||||
|
|
||||||
|
self.orientationRow = QHBoxLayout()
|
||||||
|
self.orientationRow.setObjectName(u"orientationRow")
|
||||||
|
self.inputOrientation = QRadioButton(PortEditor)
|
||||||
|
self.inputOrientation.setObjectName(u"inputOrientation")
|
||||||
|
|
||||||
|
self.orientationRow.addWidget(self.inputOrientation)
|
||||||
|
|
||||||
|
self.outputOrientation = QRadioButton(PortEditor)
|
||||||
|
self.outputOrientation.setObjectName(u"outputOrientation")
|
||||||
|
|
||||||
|
self.orientationRow.addWidget(self.outputOrientation)
|
||||||
|
|
||||||
|
|
||||||
|
self.basicForm.setLayout(2, QFormLayout.ItemRole.FieldRole, self.orientationRow)
|
||||||
|
|
||||||
|
self.sizeLabel = QLabel(PortEditor)
|
||||||
|
self.sizeLabel.setObjectName(u"sizeLabel")
|
||||||
|
|
||||||
|
self.basicForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.sizeLabel)
|
||||||
|
|
||||||
|
self.sizeRow = QHBoxLayout()
|
||||||
|
self.sizeRow.setObjectName(u"sizeRow")
|
||||||
|
self.widthSize = QSpinBox(PortEditor)
|
||||||
|
self.widthSize.setObjectName(u"widthSize")
|
||||||
|
self.widthSize.setMinimum(1)
|
||||||
|
|
||||||
|
self.sizeRow.addWidget(self.widthSize)
|
||||||
|
|
||||||
|
self.heightSize = QSpinBox(PortEditor)
|
||||||
|
self.heightSize.setObjectName(u"heightSize")
|
||||||
|
self.heightSize.setMinimum(1)
|
||||||
|
|
||||||
|
self.sizeRow.addWidget(self.heightSize)
|
||||||
|
|
||||||
|
|
||||||
|
self.basicForm.setLayout(3, QFormLayout.ItemRole.FieldRole, self.sizeRow)
|
||||||
|
|
||||||
|
self.domainLabel = QLabel(PortEditor)
|
||||||
|
self.domainLabel.setObjectName(u"domainLabel")
|
||||||
|
|
||||||
|
self.basicForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.domainLabel)
|
||||||
|
|
||||||
|
self.multiplicityCheckBox = QCheckBox(PortEditor)
|
||||||
|
self.multiplicityCheckBox.setObjectName(u"multiplicityCheckBox")
|
||||||
|
|
||||||
|
self.basicForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.multiplicityCheckBox)
|
||||||
|
|
||||||
|
self.connectionAnnotationLabel = QLabel(PortEditor)
|
||||||
|
self.connectionAnnotationLabel.setObjectName(u"connectionAnnotationLabel")
|
||||||
|
|
||||||
|
self.basicForm.setWidget(5, QFormLayout.ItemRole.LabelRole, self.connectionAnnotationLabel)
|
||||||
|
|
||||||
|
self.connectionAnnotationEdit = QLineEdit(PortEditor)
|
||||||
|
self.connectionAnnotationEdit.setObjectName(u"connectionAnnotationEdit")
|
||||||
|
|
||||||
|
self.basicForm.setWidget(5, QFormLayout.ItemRole.FieldRole, self.connectionAnnotationEdit)
|
||||||
|
|
||||||
|
|
||||||
|
self.rightColumn.addLayout(self.basicForm)
|
||||||
|
|
||||||
|
self.line = QFrame(PortEditor)
|
||||||
|
self.line.setObjectName(u"line")
|
||||||
|
self.line.setFrameShape(QFrame.Shape.HLine)
|
||||||
|
self.line.setFrameShadow(QFrame.Shadow.Sunken)
|
||||||
|
|
||||||
|
self.rightColumn.addWidget(self.line)
|
||||||
|
|
||||||
|
self.signalOptions = QFormLayout()
|
||||||
|
self.signalOptions.setObjectName(u"signalOptions")
|
||||||
|
self.signalTypeLabel = QLabel(PortEditor)
|
||||||
|
self.signalTypeLabel.setObjectName(u"signalTypeLabel")
|
||||||
|
|
||||||
|
self.signalOptions.setWidget(0, QFormLayout.ItemRole.LabelRole, self.signalTypeLabel)
|
||||||
|
|
||||||
|
self.signalTypeComboBox = QComboBox(PortEditor)
|
||||||
|
self.signalTypeComboBox.setObjectName(u"signalTypeComboBox")
|
||||||
|
|
||||||
|
self.signalOptions.setWidget(0, QFormLayout.ItemRole.FieldRole, self.signalTypeComboBox)
|
||||||
|
|
||||||
|
|
||||||
|
self.rightColumn.addLayout(self.signalOptions)
|
||||||
|
|
||||||
|
self.bondOptions = QFormLayout()
|
||||||
|
self.bondOptions.setObjectName(u"bondOptions")
|
||||||
|
self.domainLabel_2 = QLabel(PortEditor)
|
||||||
|
self.domainLabel_2.setObjectName(u"domainLabel_2")
|
||||||
|
|
||||||
|
self.bondOptions.setWidget(0, QFormLayout.ItemRole.LabelRole, self.domainLabel_2)
|
||||||
|
|
||||||
|
self.domainComboBox = QComboBox(PortEditor)
|
||||||
|
self.domainComboBox.setObjectName(u"domainComboBox")
|
||||||
|
|
||||||
|
self.bondOptions.setWidget(0, QFormLayout.ItemRole.FieldRole, self.domainComboBox)
|
||||||
|
|
||||||
|
self.causalityLabel = QLabel(PortEditor)
|
||||||
|
self.causalityLabel.setObjectName(u"causalityLabel")
|
||||||
|
|
||||||
|
self.bondOptions.setWidget(1, QFormLayout.ItemRole.LabelRole, self.causalityLabel)
|
||||||
|
|
||||||
|
self.causalityComboBox = QComboBox(PortEditor)
|
||||||
|
self.causalityComboBox.setObjectName(u"causalityComboBox")
|
||||||
|
|
||||||
|
self.bondOptions.setWidget(1, QFormLayout.ItemRole.FieldRole, self.causalityComboBox)
|
||||||
|
|
||||||
|
|
||||||
|
self.rightColumn.addLayout(self.bondOptions)
|
||||||
|
|
||||||
|
self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
||||||
|
|
||||||
|
self.rightColumn.addItem(self.verticalSpacer)
|
||||||
|
|
||||||
|
self.line_2 = QFrame(PortEditor)
|
||||||
|
self.line_2.setObjectName(u"line_2")
|
||||||
|
self.line_2.setFrameShape(QFrame.Shape.HLine)
|
||||||
|
self.line_2.setFrameShadow(QFrame.Shadow.Sunken)
|
||||||
|
|
||||||
|
self.rightColumn.addWidget(self.line_2)
|
||||||
|
|
||||||
|
self.descriptionForm = QFormLayout()
|
||||||
|
self.descriptionForm.setObjectName(u"descriptionForm")
|
||||||
|
self.descriptionForm.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||||
|
self.descriptionLabel = QLabel(PortEditor)
|
||||||
|
self.descriptionLabel.setObjectName(u"descriptionLabel")
|
||||||
|
|
||||||
|
self.descriptionForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.descriptionLabel)
|
||||||
|
|
||||||
|
self.descriptionEdit = QPlainTextEdit(PortEditor)
|
||||||
|
self.descriptionEdit.setObjectName(u"descriptionEdit")
|
||||||
|
sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.MinimumExpanding)
|
||||||
|
sizePolicy.setHorizontalStretch(0)
|
||||||
|
sizePolicy.setVerticalStretch(0)
|
||||||
|
sizePolicy.setHeightForWidth(self.descriptionEdit.sizePolicy().hasHeightForWidth())
|
||||||
|
self.descriptionEdit.setSizePolicy(sizePolicy)
|
||||||
|
self.descriptionEdit.setMinimumSize(QSize(0, 20))
|
||||||
|
self.descriptionEdit.setMaximumSize(QSize(16777215, 60))
|
||||||
|
|
||||||
|
self.descriptionForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.descriptionEdit)
|
||||||
|
|
||||||
|
|
||||||
|
self.rightColumn.addLayout(self.descriptionForm)
|
||||||
|
|
||||||
|
|
||||||
|
self.horizontalLayout_2.addLayout(self.rightColumn)
|
||||||
|
|
||||||
|
|
||||||
|
self.retranslateUi(PortEditor)
|
||||||
|
|
||||||
|
QMetaObject.connectSlotsByName(PortEditor)
|
||||||
|
# setupUi
|
||||||
|
|
||||||
|
def retranslateUi(self, PortEditor):
|
||||||
|
PortEditor.setWindowTitle(QCoreApplication.translate("PortEditor", u"Form", None))
|
||||||
|
self.addPort.setText(QCoreApplication.translate("PortEditor", u"Add Port", None))
|
||||||
|
self.removePort.setText(QCoreApplication.translate("PortEditor", u"Remove Port", None))
|
||||||
|
self.nameLabel.setText(QCoreApplication.translate("PortEditor", u"Name:", None))
|
||||||
|
self.typeLabel.setText(QCoreApplication.translate("PortEditor", u"Type:", None))
|
||||||
|
self.typeSignal.setText(QCoreApplication.translate("PortEditor", u"Signal", None))
|
||||||
|
self.typeBond.setText(QCoreApplication.translate("PortEditor", u"Power Bond", None))
|
||||||
|
self.orientationLabel.setText(QCoreApplication.translate("PortEditor", u"Orientation:", None))
|
||||||
|
self.inputOrientation.setText(QCoreApplication.translate("PortEditor", u"Input", None))
|
||||||
|
self.outputOrientation.setText(QCoreApplication.translate("PortEditor", u"Output", None))
|
||||||
|
self.sizeLabel.setText(QCoreApplication.translate("PortEditor", u"Size", None))
|
||||||
|
self.widthSize.setSuffix(QCoreApplication.translate("PortEditor", u" rows", None))
|
||||||
|
self.heightSize.setSuffix(QCoreApplication.translate("PortEditor", u" columns", None))
|
||||||
|
self.domainLabel.setText("")
|
||||||
|
self.multiplicityCheckBox.setText(QCoreApplication.translate("PortEditor", u"Allow multiple connections", None))
|
||||||
|
self.connectionAnnotationLabel.setText(QCoreApplication.translate("PortEditor", u"Connection annotation:", None))
|
||||||
|
self.connectionAnnotationEdit.setPlaceholderText(QCoreApplication.translate("PortEditor", u"For example, + or -", None))
|
||||||
|
self.signalTypeLabel.setText(QCoreApplication.translate("PortEditor", u"Signal Type:", None))
|
||||||
|
self.domainLabel_2.setText(QCoreApplication.translate("PortEditor", u"Domain:", None))
|
||||||
|
self.causalityLabel.setText(QCoreApplication.translate("PortEditor", u"Causality:", None))
|
||||||
|
self.descriptionLabel.setText(QCoreApplication.translate("PortEditor", u"Description:", None))
|
||||||
|
# retranslateUi
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ from PySide6.QtCore import QRectF, QSize, Qt
|
|||||||
from PySide6.QtGui import QBrush, QColor, QFont, QIcon, QPainter, QPen, QPixmap, QRegion
|
from PySide6.QtGui import QBrush, QColor, QFont, QIcon, QPainter, QPen, QPixmap, QRegion
|
||||||
|
|
||||||
from bedit_core.models import Port, PortID, SignalDirection
|
from bedit_core.models import Port, PortID, SignalDirection
|
||||||
from bedit_gui.models import Icon, Line, LineType, Rectangle, Text
|
from bedit_gui.models import Ellipse, Icon, Line, LineType, Rectangle, Text
|
||||||
|
|
||||||
PORT_SIZE = 16
|
PORT_SIZE = 16
|
||||||
DEFAULT_ICON_SIZE = QSize(48, 48)
|
DEFAULT_ICON_SIZE = QSize(48, 48)
|
||||||
@@ -21,7 +21,7 @@ def get_bounding_box(icon: Icon) -> QRectF:
|
|||||||
for shape in icon.shapes.values():
|
for shape in icon.shapes.values():
|
||||||
x, y = shape.pos
|
x, y = shape.pos
|
||||||
points.append((x, y))
|
points.append((x, y))
|
||||||
if isinstance(shape, (Rectangle, Text)):
|
if isinstance(shape, (Rectangle, Ellipse, Text)):
|
||||||
points.append((x + shape.width, y + shape.height))
|
points.append((x + shape.width, y + shape.height))
|
||||||
elif isinstance(shape, Line):
|
elif isinstance(shape, Line):
|
||||||
points.append(shape.end)
|
points.append(shape.end)
|
||||||
@@ -72,6 +72,10 @@ def render_icon(icon: Icon, ports: dict[PortID, Port], size: QSize = DEFAULT_ICO
|
|||||||
painter.setPen(_line_pen(shape.line_type, shape.line_thickness, shape.line_color))
|
painter.setPen(_line_pen(shape.line_type, shape.line_thickness, shape.line_color))
|
||||||
painter.setBrush(QBrush(_color(shape.fill_color)))
|
painter.setBrush(QBrush(_color(shape.fill_color)))
|
||||||
painter.drawRoundedRect(QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height), shape.corner_radius, shape.corner_radius)
|
painter.drawRoundedRect(QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height), shape.corner_radius, shape.corner_radius)
|
||||||
|
elif isinstance(shape, Ellipse):
|
||||||
|
painter.setPen(_line_pen(shape.line_type, shape.line_thickness, shape.line_color))
|
||||||
|
painter.setBrush(QBrush(_color(shape.fill_color)))
|
||||||
|
painter.drawEllipse(QRectF(shape.pos[0], shape.pos[1], shape.width, shape.height))
|
||||||
elif isinstance(shape, Text):
|
elif isinstance(shape, Text):
|
||||||
font = QFont()
|
font = QFont()
|
||||||
font.setPixelSize(max(1, round(shape.size)))
|
font.setPixelSize(max(1, round(shape.size)))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from PySide6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from bedit_core.models import Port, PortID
|
from bedit_core.models import Port, PortID
|
||||||
|
from bedit_gui.models import PortMetadata
|
||||||
from bedit_gui.views.port_editor_widget import PortEditorWidget
|
from bedit_gui.views.port_editor_widget import PortEditorWidget
|
||||||
|
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ class InterfaceEditorDialog(QDialog):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
ports: dict[PortID, Port],
|
ports: dict[PortID, Port],
|
||||||
|
port_metadata: dict[PortID, PortMetadata] | None = None,
|
||||||
parent: QWidget | None = None,
|
parent: QWidget | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -25,7 +27,7 @@ class InterfaceEditorDialog(QDialog):
|
|||||||
|
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
self.editor = PortEditorWidget(self)
|
self.editor = PortEditorWidget(self)
|
||||||
self.editor.set_ports(ports)
|
self.editor.set_ports(ports, port_metadata)
|
||||||
layout.addWidget(self.editor)
|
layout.addWidget(self.editor)
|
||||||
|
|
||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
@@ -38,3 +40,6 @@ class InterfaceEditorDialog(QDialog):
|
|||||||
|
|
||||||
def ports(self) -> dict[PortID, Port]:
|
def ports(self) -> dict[PortID, Port]:
|
||||||
return self.editor.ports()
|
return self.editor.ports()
|
||||||
|
|
||||||
|
def port_metadata(self) -> dict[PortID, PortMetadata]:
|
||||||
|
return self.editor.port_metadata()
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ from __future__ import annotations
|
|||||||
from PySide6.QtCore import QEvent, QObject, QTimer, Qt, Signal
|
from PySide6.QtCore import QEvent, QObject, QTimer, Qt, Signal
|
||||||
from PySide6.QtWidgets import QWidget
|
from PySide6.QtWidgets import QWidget
|
||||||
|
|
||||||
from bedit_core.models import Component, EquationImplementation
|
from bedit_core.models import Component, EquationImplementation, PortID
|
||||||
|
from bedit_gui.models import PortMetadata
|
||||||
from bedit_gui.ui.generated.ui_equation_editor_widget import Ui_equationEditorWidget
|
from bedit_gui.ui.generated.ui_equation_editor_widget import Ui_equationEditorWidget
|
||||||
from bedit_gui.views.param_editor_widget import ParamEditorWidget
|
from bedit_gui.views.param_editor_widget import ParamEditorWidget
|
||||||
from bedit_gui.views.port_editor_widget import PortEditorWidget
|
from bedit_gui.views.port_editor_widget import PortEditorWidget
|
||||||
@@ -14,6 +15,7 @@ class EquationEditorWidget(QWidget):
|
|||||||
|
|
||||||
component_changed = Signal(object)
|
component_changed = Signal(object)
|
||||||
equation_text_change_requested = Signal(object, str, object, int)
|
equation_text_change_requested = Signal(object, str, object, int)
|
||||||
|
port_metadata_change_requested = Signal(object, object)
|
||||||
sidebar_visible_changed = Signal(bool)
|
sidebar_visible_changed = Signal(bool)
|
||||||
|
|
||||||
def __init__(self, parent: QWidget | None = None) -> None:
|
def __init__(self, parent: QWidget | None = None) -> None:
|
||||||
@@ -22,6 +24,7 @@ class EquationEditorWidget(QWidget):
|
|||||||
self.ui = Ui_equationEditorWidget()
|
self.ui = Ui_equationEditorWidget()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self._component: Component | None = None
|
self._component: Component | None = None
|
||||||
|
self._port_metadata: dict[PortID, PortMetadata] = {}
|
||||||
self._loading = False
|
self._loading = False
|
||||||
self._edit_id = 0
|
self._edit_id = 0
|
||||||
self._edit_timer = QTimer(self)
|
self._edit_timer = QTimer(self)
|
||||||
@@ -45,11 +48,12 @@ class EquationEditorWidget(QWidget):
|
|||||||
|
|
||||||
self._set_editors_enabled(False)
|
self._set_editors_enabled(False)
|
||||||
|
|
||||||
def set_component(self, component: Component | None) -> None:
|
def set_component(self, component: Component | None, port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||||
if component is not None and not isinstance(component.implementation, EquationImplementation):
|
if component is not None and not isinstance(component.implementation, EquationImplementation):
|
||||||
raise TypeError("EquationEditorWidget only supports components with an equation implementation")
|
raise TypeError("EquationEditorWidget only supports components with an equation implementation")
|
||||||
|
|
||||||
self._component = component
|
self._component = component
|
||||||
|
self._port_metadata = port_metadata or {}
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def component(self) -> Component | None:
|
def component(self) -> Component | None:
|
||||||
@@ -88,7 +92,7 @@ class EquationEditorWidget(QWidget):
|
|||||||
self.ui.initialEquationsTextEdit.setPlainText("\n".join(implementation.initial_equations))
|
self.ui.initialEquationsTextEdit.setPlainText("\n".join(implementation.initial_equations))
|
||||||
self.ui.equationsTextEdit.setPlainText("\n".join(implementation.equations))
|
self.ui.equationsTextEdit.setPlainText("\n".join(implementation.equations))
|
||||||
self.param_editor.set_params(component.parameters)
|
self.param_editor.set_params(component.parameters)
|
||||||
self.port_editor.set_ports(component.interface.ports)
|
self.port_editor.set_ports(component.interface.ports, self._port_metadata)
|
||||||
self._loading = False
|
self._loading = False
|
||||||
self._set_editors_enabled(component is not None)
|
self._set_editors_enabled(component is not None)
|
||||||
|
|
||||||
@@ -145,8 +149,21 @@ class EquationEditorWidget(QWidget):
|
|||||||
def _ports_changed(self) -> None:
|
def _ports_changed(self) -> None:
|
||||||
if self._loading or self._component is None:
|
if self._loading or self._component is None:
|
||||||
return
|
return
|
||||||
self._component.interface.ports = self.port_editor.ports()
|
ports = self.port_editor.ports()
|
||||||
|
metadata = self.port_editor.port_metadata()
|
||||||
|
if ports != self._component.interface.ports:
|
||||||
|
self._component.interface.ports = ports
|
||||||
self.component_changed.emit(self._component)
|
self.component_changed.emit(self._component)
|
||||||
|
if metadata != self._port_metadata:
|
||||||
|
self._port_metadata = metadata
|
||||||
|
self.port_metadata_change_requested.emit(self._component, metadata)
|
||||||
|
|
||||||
|
def refresh_port_metadata(self, port_metadata: dict[PortID, PortMetadata]) -> None:
|
||||||
|
if port_metadata == self._port_metadata:
|
||||||
|
return
|
||||||
|
self._port_metadata = port_metadata
|
||||||
|
if self._component is not None:
|
||||||
|
self.port_editor.set_ports(self._component.interface.ports, port_metadata)
|
||||||
|
|
||||||
def _replace_placeholder(self, placeholder: QWidget, editor: QWidget) -> None:
|
def _replace_placeholder(self, placeholder: QWidget, editor: QWidget) -> None:
|
||||||
self.ui.verticalLayout_2.replaceWidget(placeholder, editor)
|
self.ui.verticalLayout_2.replaceWidget(placeholder, editor)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from PySide6.QtGui import QActionGroup, QBrush, QColor, QCursor, QDragEnterEvent
|
|||||||
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsTextItem, QGraphicsView, QMenu, QWidget
|
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsPathItem, QGraphicsPixmapItem, QGraphicsScene, QGraphicsSceneMouseEvent, QGraphicsTextItem, QGraphicsView, QMenu, QWidget
|
||||||
|
|
||||||
from bedit_core.models import BondCausality, BondConnection, BondPort, Component, ComponentID, Connection, ConnectionID, GraphImplementation, Port, PortID, SignalConnection, SignalDirection, SignalPort
|
from bedit_core.models import BondCausality, BondConnection, BondPort, Component, ComponentID, Connection, ConnectionID, GraphImplementation, Port, PortID, SignalConnection, SignalDirection, SignalPort
|
||||||
from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, Icon
|
from bedit_gui.models import Graph, GraphComponentLabel, GraphConnection, Icon, PortMetadata
|
||||||
from bedit_gui.services.component_clipboard import COMPONENTS_MIME
|
from bedit_gui.services.component_clipboard import COMPONENTS_MIME
|
||||||
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_natural_icon_size, get_pixmap_bounding_box, render_icon
|
from bedit_gui.utils.icon import get_natural_icon_size, get_pixmap_bounding_box, render_icon
|
||||||
@@ -34,6 +34,9 @@ ARROW_HALF_WIDTH = 16.0
|
|||||||
SIGNAL_ARROW_LENGTH = ARROW_LENGTH / 2
|
SIGNAL_ARROW_LENGTH = ARROW_LENGTH / 2
|
||||||
SIGNAL_ARROW_HALF_WIDTH = ARROW_HALF_WIDTH / 2
|
SIGNAL_ARROW_HALF_WIDTH = ARROW_HALF_WIDTH / 2
|
||||||
CAUSALITY_TICK_HALF_LENGTH = 16.0
|
CAUSALITY_TICK_HALF_LENGTH = 16.0
|
||||||
|
CONNECTION_ANNOTATION_FONT_SIZE = 18.0
|
||||||
|
CONNECTION_ANNOTATION_BACK_OFFSET = 24.0
|
||||||
|
CONNECTION_ANNOTATION_SIDE_OFFSET = 14.0
|
||||||
|
|
||||||
|
|
||||||
class GraphEditorMode(Enum):
|
class GraphEditorMode(Enum):
|
||||||
@@ -152,7 +155,7 @@ class GraphComponentItem(QGraphicsPixmapItem):
|
|||||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)
|
||||||
|
|
||||||
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object:
|
def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: object) -> object:
|
||||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self._dragging and isinstance(value, QPointF):
|
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange and self.component_id in self.editor._component_drag_starts and isinstance(value, QPointF):
|
||||||
grid_size = self.editor.snap_to_grid_size
|
grid_size = self.editor.snap_to_grid_size
|
||||||
value = QPointF(round(value.x() / grid_size) * grid_size, round(value.y() / grid_size) * grid_size)
|
value = QPointF(round(value.x() / grid_size) * grid_size, round(value.y() / grid_size) * grid_size)
|
||||||
result = super().itemChange(change, value)
|
result = super().itemChange(change, value)
|
||||||
@@ -162,23 +165,22 @@ class GraphComponentItem(QGraphicsPixmapItem):
|
|||||||
|
|
||||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||||
if event.button() == Qt.MouseButton.LeftButton and self.editor.mode is GraphEditorMode.CONNECTION:
|
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()
|
event.accept()
|
||||||
return
|
return
|
||||||
self._drag_start = QPointF(self.pos())
|
self._drag_start = QPointF(self.pos())
|
||||||
self._dragging = True
|
self._dragging = True
|
||||||
super().mousePressEvent(event)
|
super().mousePressEvent(event)
|
||||||
|
self.editor.begin_component_move()
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||||
if self.editor.mode is GraphEditorMode.CONNECTION:
|
if self.editor.mode is GraphEditorMode.CONNECTION:
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self.editor.choose_connection_component(self.component_id, event.screenPos())
|
||||||
event.accept()
|
event.accept()
|
||||||
return
|
return
|
||||||
super().mouseReleaseEvent(event)
|
super().mouseReleaseEvent(event)
|
||||||
self._dragging = False
|
self._dragging = False
|
||||||
position = (round(self.pos().x()), round(self.pos().y()))
|
self.editor.finish_component_moves()
|
||||||
self.setPos(*position)
|
|
||||||
if self.pos() != self._drag_start:
|
|
||||||
self.editor.finish_component_move(self.component_id, position)
|
|
||||||
|
|
||||||
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
def mouseDoubleClickEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||||
if event.button() == Qt.MouseButton.LeftButton:
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
@@ -294,7 +296,7 @@ class GraphConnectionPointItem(QGraphicsEllipseItem):
|
|||||||
|
|
||||||
|
|
||||||
class GraphEditorWidget(QWidget):
|
class GraphEditorWidget(QWidget):
|
||||||
component_move_requested = Signal(object, object, object)
|
component_moves_requested = Signal(object, object)
|
||||||
component_context_menu_requested = Signal(object, object)
|
component_context_menu_requested = Signal(object, object)
|
||||||
component_open_requested = Signal(object)
|
component_open_requested = Signal(object)
|
||||||
component_label_move_requested = Signal(object, object, object)
|
component_label_move_requested = Signal(object, object, object)
|
||||||
@@ -311,14 +313,17 @@ class GraphEditorWidget(QWidget):
|
|||||||
self._component: Component | None = None
|
self._component: Component | None = None
|
||||||
self._graph = Graph()
|
self._graph = Graph()
|
||||||
self._icons: dict[ComponentID, Icon] = {}
|
self._icons: dict[ComponentID, Icon] = {}
|
||||||
|
self._port_metadata: dict[PortID, PortMetadata] = {}
|
||||||
self._mode = GraphEditorMode.NORMAL
|
self._mode = GraphEditorMode.NORMAL
|
||||||
self._connection_start: ComponentID | None = None
|
self._connection_start: ComponentID | None = None
|
||||||
self._connection_preview: QGraphicsPathItem | None = None
|
self._connection_preview: QGraphicsPathItem | None = None
|
||||||
self._component_items: dict[ComponentID, GraphComponentItem] = {}
|
self._component_items: dict[ComponentID, GraphComponentItem] = {}
|
||||||
|
self._component_drag_starts: dict[ComponentID, tuple[int, int]] = {}
|
||||||
self._component_bounds: dict[ComponentID, QRectF] = {}
|
self._component_bounds: dict[ComponentID, QRectF] = {}
|
||||||
self._component_label_items: dict[ComponentID, GraphComponentLabelItem] = {}
|
self._component_label_items: dict[ComponentID, GraphComponentLabelItem] = {}
|
||||||
self._connection_items: dict[ConnectionID, GraphConnectionItem] = {}
|
self._connection_items: dict[ConnectionID, GraphConnectionItem] = {}
|
||||||
self._connection_point_items: dict[ConnectionID, list[GraphConnectionPointItem]] = {}
|
self._connection_point_items: dict[ConnectionID, list[GraphConnectionPointItem]] = {}
|
||||||
|
self._connection_annotation_items: dict[ConnectionID, QGraphicsTextItem] = {}
|
||||||
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)
|
||||||
@@ -363,12 +368,12 @@ class GraphEditorWidget(QWidget):
|
|||||||
self._clear_connection_start()
|
self._clear_connection_start()
|
||||||
component = self._component
|
component = self._component
|
||||||
if component is not None:
|
if component is not None:
|
||||||
self.set_component(component, self._graph, self._icons)
|
self.set_component(component, self._graph, self._icons, self._port_metadata)
|
||||||
|
|
||||||
def toggle_mode(self) -> None:
|
def toggle_mode(self) -> None:
|
||||||
self.set_mode(GraphEditorMode.CONNECTION if self._mode is GraphEditorMode.NORMAL else GraphEditorMode.NORMAL)
|
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:
|
def set_component(self, component: Component | None, graph: Graph | None = None, icons: dict[ComponentID, Icon] | None = None, port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||||
if component is not None and not isinstance(component.implementation, GraphImplementation):
|
if component is not None and not isinstance(component.implementation, GraphImplementation):
|
||||||
raise TypeError("GraphEditorWidget only supports components with a graph implementation")
|
raise TypeError("GraphEditorWidget only supports components with a graph implementation")
|
||||||
component_changed = component is not self._component
|
component_changed = component is not self._component
|
||||||
@@ -376,11 +381,14 @@ class GraphEditorWidget(QWidget):
|
|||||||
self._component = component
|
self._component = component
|
||||||
self._graph = graph or Graph()
|
self._graph = graph or Graph()
|
||||||
self._icons = icons or {}
|
self._icons = icons or {}
|
||||||
|
self._port_metadata = port_metadata or {}
|
||||||
|
self._component_drag_starts = {}
|
||||||
self._component_items = {}
|
self._component_items = {}
|
||||||
self._component_bounds = {}
|
self._component_bounds = {}
|
||||||
self._component_label_items = {}
|
self._component_label_items = {}
|
||||||
self._connection_items = {}
|
self._connection_items = {}
|
||||||
self._connection_point_items = {}
|
self._connection_point_items = {}
|
||||||
|
self._connection_annotation_items = {}
|
||||||
self.scene.clear()
|
self.scene.clear()
|
||||||
if component is None:
|
if component is None:
|
||||||
return
|
return
|
||||||
@@ -419,6 +427,17 @@ class GraphEditorWidget(QWidget):
|
|||||||
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)
|
||||||
|
annotation = self._port_metadata.get(connection.target, PortMetadata()).connection_annotation if isinstance(connection, SignalConnection) else None
|
||||||
|
if annotation:
|
||||||
|
annotation_item = QGraphicsTextItem(annotation)
|
||||||
|
font = annotation_item.font()
|
||||||
|
font.setBold(True)
|
||||||
|
font.setPointSizeF(CONNECTION_ANNOTATION_FONT_SIZE)
|
||||||
|
annotation_item.setFont(font)
|
||||||
|
annotation_item.setDefaultTextColor(QColor(SIGNAL_CONNECTION_COLOR))
|
||||||
|
annotation_item.setZValue(1)
|
||||||
|
self._connection_annotation_items[connection_id] = annotation_item
|
||||||
|
self.scene.addItem(annotation_item)
|
||||||
visual_connection = graph.connections.get(connection_id)
|
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._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()
|
||||||
@@ -451,6 +470,23 @@ class GraphEditorWidget(QWidget):
|
|||||||
if isinstance(connection, BondConnection):
|
if isinstance(connection, BondConnection):
|
||||||
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))
|
||||||
|
annotation_item = self._connection_annotation_items.get(connection_id)
|
||||||
|
if annotation_item is not None:
|
||||||
|
self._position_connection_annotation(annotation_item, points)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _position_connection_annotation(item: QGraphicsTextItem, points: list[tuple[float, float]]) -> None:
|
||||||
|
target = QPointF(*points[-1])
|
||||||
|
previous = next((QPointF(*point) for point in reversed(points[:-1]) if QPointF(*point) != target), None)
|
||||||
|
if previous is None:
|
||||||
|
return
|
||||||
|
dx = target.x() - previous.x()
|
||||||
|
dy = target.y() - previous.y()
|
||||||
|
length = hypot(dx, dy)
|
||||||
|
x = target.x() - CONNECTION_ANNOTATION_BACK_OFFSET * dx / length - CONNECTION_ANNOTATION_SIDE_OFFSET * dy / length
|
||||||
|
y = target.y() - CONNECTION_ANNOTATION_BACK_OFFSET * dy / length + CONNECTION_ANNOTATION_SIDE_OFFSET * dx / length
|
||||||
|
bounds = item.boundingRect()
|
||||||
|
item.setPos(x - bounds.width() / 2, y - bounds.height() / 2)
|
||||||
|
|
||||||
def add_connection_point(self, connection_id: ConnectionID, scene_position: QPointF) -> None:
|
def add_connection_point(self, connection_id: ConnectionID, scene_position: QPointF) -> None:
|
||||||
points = self._connection_metadata_points(connection_id)
|
points = self._connection_metadata_points(connection_id)
|
||||||
@@ -476,6 +512,9 @@ class GraphEditorWidget(QWidget):
|
|||||||
connection_item = self._connection_items.pop(connection_id, None)
|
connection_item = self._connection_items.pop(connection_id, None)
|
||||||
if connection_item is not None:
|
if connection_item is not None:
|
||||||
self.scene.removeItem(connection_item)
|
self.scene.removeItem(connection_item)
|
||||||
|
annotation_item = self._connection_annotation_items.pop(connection_id, None)
|
||||||
|
if annotation_item is not None:
|
||||||
|
self.scene.removeItem(annotation_item)
|
||||||
return
|
return
|
||||||
self._graph.connections[connection_id] = GraphConnection(points=list(points))
|
self._graph.connections[connection_id] = GraphConnection(points=list(points))
|
||||||
interior_points = points[1:-1]
|
interior_points = points[1:-1]
|
||||||
@@ -531,9 +570,23 @@ class GraphEditorWidget(QWidget):
|
|||||||
best_distance = distance
|
best_distance = distance
|
||||||
return best_index
|
return best_index
|
||||||
|
|
||||||
def finish_component_move(self, component_id: ComponentID, position: tuple[int, int]) -> None:
|
def begin_component_move(self) -> None:
|
||||||
if self._component is not None:
|
self._component_drag_starts = {item.component_id: (round(item.pos().x()), round(item.pos().y())) for item in self.scene.selectedItems() if isinstance(item, GraphComponentItem)}
|
||||||
self.component_move_requested.emit(self._component, component_id, position)
|
|
||||||
|
def finish_component_moves(self) -> None:
|
||||||
|
starts = self._component_drag_starts
|
||||||
|
self._component_drag_starts = {}
|
||||||
|
if self._component is None:
|
||||||
|
return
|
||||||
|
positions = {}
|
||||||
|
for component_id, old_position in starts.items():
|
||||||
|
item = self._component_items[component_id]
|
||||||
|
position = (round(item.pos().x()), round(item.pos().y()))
|
||||||
|
item.setPos(*position)
|
||||||
|
if position != old_position:
|
||||||
|
positions[component_id] = position
|
||||||
|
if positions:
|
||||||
|
self.component_moves_requested.emit(self._component, positions)
|
||||||
|
|
||||||
def finish_component_label_move(self, component_id: ComponentID, relative_position: tuple[int, int]) -> None:
|
def finish_component_label_move(self, component_id: ComponentID, relative_position: tuple[int, int]) -> None:
|
||||||
if self._component is not None:
|
if self._component is not None:
|
||||||
@@ -723,7 +776,7 @@ class GraphEditorWidget(QWidget):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _port_available(port_id: PortID, port: Port, used_ports: set[PortID]) -> bool:
|
def _port_available(port_id: PortID, port: Port, used_ports: set[PortID]) -> bool:
|
||||||
if isinstance(port, SignalPort):
|
if isinstance(port, SignalPort):
|
||||||
return port.direction is SignalDirection.OUTPUT or port_id not in used_ports
|
return port.direction is SignalDirection.OUTPUT or port.multiplicity or port_id not in used_ports
|
||||||
if isinstance(port, BondPort):
|
if isinstance(port, BondPort):
|
||||||
return port.multiplicity or port_id not in used_ports
|
return port.multiplicity or port_id not in used_ports
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from bedit_gui.models import Icon, Shape, ShapeID
|
|||||||
from bedit_gui.services import icon_files
|
from bedit_gui.services import icon_files
|
||||||
from bedit_gui.services.application_logging import get_logger
|
from bedit_gui.services.application_logging import get_logger
|
||||||
from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor
|
from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor
|
||||||
from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, LineCreationTool, RectangleCreationTool, TextCreationTool
|
from bedit_gui.views.icon_graphics_scene import EllipseCreationTool, IconGraphicsScene, LineCreationTool, RectangleCreationTool, TextCreationTool
|
||||||
from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog
|
from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -123,6 +123,8 @@ class IconEditorWindow(QMainWindow):
|
|||||||
self.ui.graphicsView.viewport().installEventFilter(self)
|
self.ui.graphicsView.viewport().installEventFilter(self)
|
||||||
self.ui.actionAdd_Rectangle.setCheckable(True)
|
self.ui.actionAdd_Rectangle.setCheckable(True)
|
||||||
self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool)
|
self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool)
|
||||||
|
self.ui.actionAdd_Circle.setCheckable(True)
|
||||||
|
self.ui.actionAdd_Circle.triggered.connect(self._start_ellipse_tool)
|
||||||
self.ui.actionAdd_Text.setCheckable(True)
|
self.ui.actionAdd_Text.setCheckable(True)
|
||||||
self.ui.actionAdd_Text.triggered.connect(self._start_text_tool)
|
self.ui.actionAdd_Text.triggered.connect(self._start_text_tool)
|
||||||
self.ui.actionAdd_Line.setCheckable(True)
|
self.ui.actionAdd_Line.setCheckable(True)
|
||||||
@@ -306,14 +308,24 @@ class IconEditorWindow(QMainWindow):
|
|||||||
|
|
||||||
def _start_rectangle_tool(self) -> None:
|
def _start_rectangle_tool(self) -> None:
|
||||||
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
||||||
|
self.ui.actionAdd_Circle.setChecked(False)
|
||||||
self.ui.actionAdd_Text.setChecked(False)
|
self.ui.actionAdd_Text.setChecked(False)
|
||||||
self.ui.actionAdd_Line.setChecked(False)
|
self.ui.actionAdd_Line.setChecked(False)
|
||||||
self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer))
|
self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer))
|
||||||
self.ui.actionAdd_Rectangle.setChecked(True)
|
self.ui.actionAdd_Rectangle.setChecked(True)
|
||||||
|
|
||||||
|
def _start_ellipse_tool(self) -> None:
|
||||||
|
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
||||||
|
self.ui.actionAdd_Rectangle.setChecked(False)
|
||||||
|
self.ui.actionAdd_Text.setChecked(False)
|
||||||
|
self.ui.actionAdd_Line.setChecked(False)
|
||||||
|
self.scene.set_creation_tool(EllipseCreationTool(self.scene, layer))
|
||||||
|
self.ui.actionAdd_Circle.setChecked(True)
|
||||||
|
|
||||||
def _start_text_tool(self) -> None:
|
def _start_text_tool(self) -> None:
|
||||||
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
||||||
self.ui.actionAdd_Rectangle.setChecked(False)
|
self.ui.actionAdd_Rectangle.setChecked(False)
|
||||||
|
self.ui.actionAdd_Circle.setChecked(False)
|
||||||
self.ui.actionAdd_Line.setChecked(False)
|
self.ui.actionAdd_Line.setChecked(False)
|
||||||
self.scene.set_creation_tool(TextCreationTool(self.scene, layer))
|
self.scene.set_creation_tool(TextCreationTool(self.scene, layer))
|
||||||
self.ui.actionAdd_Text.setChecked(True)
|
self.ui.actionAdd_Text.setChecked(True)
|
||||||
@@ -321,6 +333,7 @@ class IconEditorWindow(QMainWindow):
|
|||||||
def _start_line_tool(self) -> None:
|
def _start_line_tool(self) -> None:
|
||||||
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1
|
||||||
self.ui.actionAdd_Rectangle.setChecked(False)
|
self.ui.actionAdd_Rectangle.setChecked(False)
|
||||||
|
self.ui.actionAdd_Circle.setChecked(False)
|
||||||
self.ui.actionAdd_Text.setChecked(False)
|
self.ui.actionAdd_Text.setChecked(False)
|
||||||
self.scene.set_creation_tool(LineCreationTool(self.scene, layer))
|
self.scene.set_creation_tool(LineCreationTool(self.scene, layer))
|
||||||
self.ui.actionAdd_Line.setChecked(True)
|
self.ui.actionAdd_Line.setChecked(True)
|
||||||
@@ -328,6 +341,7 @@ class IconEditorWindow(QMainWindow):
|
|||||||
def _tool_active_changed(self, active: bool) -> None:
|
def _tool_active_changed(self, active: bool) -> None:
|
||||||
if not active:
|
if not active:
|
||||||
self.ui.actionAdd_Rectangle.setChecked(False)
|
self.ui.actionAdd_Rectangle.setChecked(False)
|
||||||
|
self.ui.actionAdd_Circle.setChecked(False)
|
||||||
self.ui.actionAdd_Text.setChecked(False)
|
self.ui.actionAdd_Text.setChecked(False)
|
||||||
self.ui.actionAdd_Line.setChecked(False)
|
self.ui.actionAdd_Line.setChecked(False)
|
||||||
cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor
|
cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ from typing import Protocol
|
|||||||
|
|
||||||
from PySide6.QtCore import QLineF, QObject, QPointF, QRectF, Qt, Signal
|
from PySide6.QtCore import QLineF, QObject, QPointF, QRectF, Qt, Signal
|
||||||
from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen
|
from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen
|
||||||
from PySide6.QtWidgets import QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
|
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsLineItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget
|
||||||
|
|
||||||
from bedit_core.models import Port, PortID, SignalDirection
|
from bedit_core.models import Port, PortID, SignalDirection
|
||||||
from bedit_gui.models import Icon, Line, LineType, Rectangle, Shape, ShapeID, Text
|
from bedit_gui.models import Ellipse, Icon, Line, LineType, Rectangle, Shape, ShapeID, Text
|
||||||
|
|
||||||
ICON_SCENE_SIZE = 512
|
ICON_SCENE_SIZE = 512
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ class RectangleCreationTool:
|
|||||||
self.scene = scene
|
self.scene = scene
|
||||||
self.layer = layer
|
self.layer = layer
|
||||||
self.start: QPointF | None = None
|
self.start: QPointF | None = None
|
||||||
self.preview: QGraphicsRectItem | None = None
|
self.preview: QGraphicsRectItem | QGraphicsEllipseItem | None = None
|
||||||
|
|
||||||
def begin(self, position: QPointF) -> None:
|
def begin(self, position: QPointF) -> None:
|
||||||
position = self._bounded(position)
|
position = self._bounded(position)
|
||||||
@@ -69,6 +69,16 @@ class TextCreationTool(RectangleCreationTool):
|
|||||||
return Text(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height(), text="Text")
|
return Text(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height(), text="Text")
|
||||||
|
|
||||||
|
|
||||||
|
class EllipseCreationTool(RectangleCreationTool):
|
||||||
|
def begin(self, position: QPointF) -> None:
|
||||||
|
position = self._bounded(position)
|
||||||
|
self.start = position
|
||||||
|
self.preview = self.scene.addEllipse(QRectF(position, position), QPen(Qt.PenStyle.DashLine))
|
||||||
|
|
||||||
|
def create_shape(self, rect: QRectF) -> Shape:
|
||||||
|
return Ellipse(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height())
|
||||||
|
|
||||||
|
|
||||||
class LineCreationTool:
|
class LineCreationTool:
|
||||||
def __init__(self, scene: QGraphicsScene, layer: int) -> None:
|
def __init__(self, scene: QGraphicsScene, layer: int) -> None:
|
||||||
self.scene = scene
|
self.scene = scene
|
||||||
@@ -109,7 +119,8 @@ class LineCreationTool:
|
|||||||
|
|
||||||
|
|
||||||
class ShapeGraphicsItem(QGraphicsPathItem):
|
class ShapeGraphicsItem(QGraphicsPathItem):
|
||||||
handle_size = 6.0
|
handle_size = 8.0
|
||||||
|
handle_hit_size = 16.0
|
||||||
|
|
||||||
def __init__(self, shape_id: ShapeID, shape: Shape, scene: IconGraphicsScene) -> None:
|
def __init__(self, shape_id: ShapeID, shape: Shape, scene: IconGraphicsScene) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -128,13 +139,24 @@ class ShapeGraphicsItem(QGraphicsPathItem):
|
|||||||
def resize_handles(self) -> dict[str, QRectF]:
|
def resize_handles(self) -> dict[str, QRectF]:
|
||||||
return {"size": self.resize_handle_rect()}
|
return {"size": self.resize_handle_rect()}
|
||||||
|
|
||||||
|
def resize_handle_hit_rects(self) -> dict[str, QRectF]:
|
||||||
|
size = self.handle_hit_size
|
||||||
|
return {name: QRectF(rect.center().x() - size / 2, rect.center().y() - size / 2, size, size) for name, rect in self.resize_handles().items()}
|
||||||
|
|
||||||
def boundingRect(self) -> QRectF:
|
def boundingRect(self) -> QRectF:
|
||||||
margin = self.handle_size / 2
|
margin = self.handle_hit_size / 2
|
||||||
return super().boundingRect().adjusted(-margin, -margin, margin, margin)
|
return super().boundingRect().adjusted(-margin, -margin, margin, margin)
|
||||||
|
|
||||||
|
def shape(self) -> QPainterPath:
|
||||||
|
path = super().shape()
|
||||||
|
if self.isSelected():
|
||||||
|
for rect in self.resize_handle_hit_rects().values():
|
||||||
|
path.addRect(rect)
|
||||||
|
return path
|
||||||
|
|
||||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
|
||||||
self._original_shape = self.current_shape()
|
self._original_shape = self.current_shape()
|
||||||
self._resize_handle = next((name for name, rect in self.resize_handles().items() if self.isSelected() and rect.contains(event.pos())), None)
|
self._resize_handle = next((name for name, rect in self.resize_handle_hit_rects().items() if self.isSelected() and rect.contains(event.pos())), None)
|
||||||
if self._resize_handle is not None:
|
if self._resize_handle is not None:
|
||||||
event.accept()
|
event.accept()
|
||||||
return
|
return
|
||||||
@@ -226,6 +248,37 @@ class RectangleGraphicsItem(ShapeGraphicsItem):
|
|||||||
self.setPath(path)
|
self.setPath(path)
|
||||||
|
|
||||||
|
|
||||||
|
class EllipseGraphicsItem(ShapeGraphicsItem):
|
||||||
|
def __init__(self, shape_id: ShapeID, shape: Ellipse, scene: IconGraphicsScene) -> None:
|
||||||
|
super().__init__(shape_id, shape, scene)
|
||||||
|
self.ellipse = deepcopy(shape)
|
||||||
|
self.setPos(shape.pos[0], shape.pos[1])
|
||||||
|
self._set_size(shape.width, shape.height)
|
||||||
|
self.setPen(scene._pen(shape))
|
||||||
|
self.setBrush(QBrush(scene._color(shape.fill_color)))
|
||||||
|
self.setZValue(shape.layer)
|
||||||
|
|
||||||
|
def current_shape(self) -> Ellipse:
|
||||||
|
shape = deepcopy(self.ellipse)
|
||||||
|
shape.pos = (round(self.pos().x()), round(self.pos().y()))
|
||||||
|
rect = self.path().boundingRect()
|
||||||
|
shape.width = rect.width()
|
||||||
|
shape.height = rect.height()
|
||||||
|
return shape
|
||||||
|
|
||||||
|
def resize_to(self, position: QPointF, _handle: str) -> None:
|
||||||
|
bounds = self.icon_scene.sceneRect()
|
||||||
|
width = max(1.0, min(bounds.right(), round(position.x())) - self.pos().x())
|
||||||
|
height = max(1.0, min(bounds.bottom(), round(position.y())) - self.pos().y())
|
||||||
|
self._set_size(width, height)
|
||||||
|
|
||||||
|
def _set_size(self, width: float, height: float) -> None:
|
||||||
|
self.prepareGeometryChange()
|
||||||
|
path = QPainterPath()
|
||||||
|
path.addEllipse(QRectF(0, 0, width, height))
|
||||||
|
self.setPath(path)
|
||||||
|
|
||||||
|
|
||||||
class TextGraphicsItem(ShapeGraphicsItem):
|
class TextGraphicsItem(ShapeGraphicsItem):
|
||||||
def __init__(self, shape_id: ShapeID, shape: Text, scene: IconGraphicsScene) -> None:
|
def __init__(self, shape_id: ShapeID, shape: Text, scene: IconGraphicsScene) -> None:
|
||||||
super().__init__(shape_id, shape, scene)
|
super().__init__(shape_id, shape, scene)
|
||||||
@@ -440,13 +493,15 @@ class IconGraphicsScene(QGraphicsScene):
|
|||||||
def _add_shape_item(self, shape_id: ShapeID, shape: Shape) -> None:
|
def _add_shape_item(self, shape_id: ShapeID, shape: Shape) -> None:
|
||||||
if isinstance(shape, Rectangle):
|
if isinstance(shape, Rectangle):
|
||||||
self.addItem(RectangleGraphicsItem(shape_id, shape, self))
|
self.addItem(RectangleGraphicsItem(shape_id, shape, self))
|
||||||
|
elif isinstance(shape, Ellipse):
|
||||||
|
self.addItem(EllipseGraphicsItem(shape_id, shape, self))
|
||||||
elif isinstance(shape, Text):
|
elif isinstance(shape, Text):
|
||||||
self.addItem(TextGraphicsItem(shape_id, shape, self))
|
self.addItem(TextGraphicsItem(shape_id, shape, self))
|
||||||
elif isinstance(shape, Line):
|
elif isinstance(shape, Line):
|
||||||
self.addItem(LineGraphicsItem(shape_id, shape, self))
|
self.addItem(LineGraphicsItem(shape_id, shape, self))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _pen(shape: Rectangle) -> QPen:
|
def _pen(shape: Rectangle | Ellipse) -> QPen:
|
||||||
return IconGraphicsScene._line_pen(shape.line_type, shape.line_thickness, shape.line_color)
|
return IconGraphicsScene._line_pen(shape.line_type, shape.line_thickness, shape.line_color)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from PySide6.QtGui import QStandardItem, QStandardItemModel
|
|||||||
from PySide6.QtWidgets import QButtonGroup, QLayout, QWidget
|
from PySide6.QtWidgets import QButtonGroup, QLayout, QWidget
|
||||||
|
|
||||||
from bedit_core.models import BondPort, Port, PortCausality, PortID, SignalDirection, SignalPort, ValueType
|
from bedit_core.models import BondPort, Port, PortCausality, PortID, SignalDirection, SignalPort, ValueType
|
||||||
|
from bedit_gui.models import PortMetadata
|
||||||
from bedit_gui.ui.generated.ui_port_editor_widget import Ui_PortEditor
|
from bedit_gui.ui.generated.ui_port_editor_widget import Ui_PortEditor
|
||||||
|
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ class PortEditorWidget(QWidget):
|
|||||||
self.ui = Ui_PortEditor()
|
self.ui = Ui_PortEditor()
|
||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self._ports: dict[PortID, Port] = {}
|
self._ports: dict[PortID, Port] = {}
|
||||||
|
self._port_metadata: dict[PortID, PortMetadata] = {}
|
||||||
self._port_ids: list[PortID] = []
|
self._port_ids: list[PortID] = []
|
||||||
self._loading = False
|
self._loading = False
|
||||||
|
|
||||||
@@ -54,6 +56,7 @@ class PortEditorWidget(QWidget):
|
|||||||
self.ui.widthSize.valueChanged.connect(self._form_changed)
|
self.ui.widthSize.valueChanged.connect(self._form_changed)
|
||||||
self.ui.heightSize.valueChanged.connect(self._form_changed)
|
self.ui.heightSize.valueChanged.connect(self._form_changed)
|
||||||
self.ui.multiplicityCheckBox.toggled.connect(self._form_changed)
|
self.ui.multiplicityCheckBox.toggled.connect(self._form_changed)
|
||||||
|
self.ui.connectionAnnotationEdit.textChanged.connect(self._form_changed)
|
||||||
self.ui.signalTypeComboBox.currentIndexChanged.connect(self._form_changed)
|
self.ui.signalTypeComboBox.currentIndexChanged.connect(self._form_changed)
|
||||||
self.ui.domainComboBox.currentTextChanged.connect(self._form_changed)
|
self.ui.domainComboBox.currentTextChanged.connect(self._form_changed)
|
||||||
self.ui.causalityComboBox.currentIndexChanged.connect(self._form_changed)
|
self.ui.causalityComboBox.currentIndexChanged.connect(self._form_changed)
|
||||||
@@ -62,14 +65,18 @@ class PortEditorWidget(QWidget):
|
|||||||
self._set_editor_enabled(False)
|
self._set_editor_enabled(False)
|
||||||
self._update_option_visibility()
|
self._update_option_visibility()
|
||||||
|
|
||||||
def set_ports(self, ports: dict[PortID, Port]) -> None:
|
def set_ports(self, ports: dict[PortID, Port], port_metadata: dict[PortID, PortMetadata] | None = None) -> None:
|
||||||
self._ports = deepcopy(ports)
|
self._ports = deepcopy(ports)
|
||||||
|
self._port_metadata = deepcopy(port_metadata or {})
|
||||||
self._port_ids = list(self._ports)
|
self._port_ids = list(self._ports)
|
||||||
self._rebuild_list()
|
self._rebuild_list()
|
||||||
|
|
||||||
def ports(self) -> dict[PortID, Port]:
|
def ports(self) -> dict[PortID, Port]:
|
||||||
return deepcopy(self._ports)
|
return deepcopy(self._ports)
|
||||||
|
|
||||||
|
def port_metadata(self) -> dict[PortID, PortMetadata]:
|
||||||
|
return deepcopy(self._port_metadata)
|
||||||
|
|
||||||
def _rebuild_list(self, selected_id: PortID | None = None) -> None:
|
def _rebuild_list(self, selected_id: PortID | None = None) -> None:
|
||||||
self._list_model.clear()
|
self._list_model.clear()
|
||||||
for port_id in self._port_ids:
|
for port_id in self._port_ids:
|
||||||
@@ -90,7 +97,7 @@ class PortEditorWidget(QWidget):
|
|||||||
self.ui.removePort.setEnabled(port_id is not None)
|
self.ui.removePort.setEnabled(port_id is not None)
|
||||||
self._set_editor_enabled(port_id is not None)
|
self._set_editor_enabled(port_id is not None)
|
||||||
if port_id is not None:
|
if port_id is not None:
|
||||||
self._load_port(self._ports[port_id])
|
self._load_port(self._ports[port_id], self._port_metadata.get(port_id, PortMetadata()))
|
||||||
|
|
||||||
def _selected_port_id(self) -> PortID | None:
|
def _selected_port_id(self) -> PortID | None:
|
||||||
index = self.ui.portList.currentIndex()
|
index = self.ui.portList.currentIndex()
|
||||||
@@ -98,7 +105,7 @@ class PortEditorWidget(QWidget):
|
|||||||
return None
|
return None
|
||||||
return self._port_ids[index.row()]
|
return self._port_ids[index.row()]
|
||||||
|
|
||||||
def _load_port(self, port: Port) -> None:
|
def _load_port(self, port: Port, metadata: PortMetadata) -> None:
|
||||||
self._loading = True
|
self._loading = True
|
||||||
self.ui.nameEdit.setText(port.name)
|
self.ui.nameEdit.setText(port.name)
|
||||||
self.ui.inputOrientation.setChecked(port.direction is SignalDirection.INPUT)
|
self.ui.inputOrientation.setChecked(port.direction is SignalDirection.INPUT)
|
||||||
@@ -106,6 +113,7 @@ class PortEditorWidget(QWidget):
|
|||||||
self.ui.widthSize.setValue(port.matrix_size[0])
|
self.ui.widthSize.setValue(port.matrix_size[0])
|
||||||
self.ui.heightSize.setValue(port.matrix_size[1])
|
self.ui.heightSize.setValue(port.matrix_size[1])
|
||||||
self.ui.multiplicityCheckBox.setChecked(port.multiplicity)
|
self.ui.multiplicityCheckBox.setChecked(port.multiplicity)
|
||||||
|
self.ui.connectionAnnotationEdit.setText(metadata.connection_annotation or "")
|
||||||
self.ui.descriptionEdit.setPlainText(port.description or "")
|
self.ui.descriptionEdit.setPlainText(port.description or "")
|
||||||
|
|
||||||
if isinstance(port, SignalPort):
|
if isinstance(port, SignalPort):
|
||||||
@@ -135,6 +143,12 @@ class PortEditorWidget(QWidget):
|
|||||||
|
|
||||||
old_port = self._ports[port_id]
|
old_port = self._ports[port_id]
|
||||||
self._ports[port_id] = self._port_from_form(old_port)
|
self._ports[port_id] = self._port_from_form(old_port)
|
||||||
|
annotation = (self.ui.connectionAnnotationEdit.text().strip() or None) if self.ui.multiplicityCheckBox.isChecked() else None
|
||||||
|
metadata = PortMetadata(connection_annotation=annotation)
|
||||||
|
if metadata == PortMetadata():
|
||||||
|
self._port_metadata.pop(port_id, None)
|
||||||
|
else:
|
||||||
|
self._port_metadata[port_id] = metadata
|
||||||
self._list_model.item(self._port_ids.index(port_id)).setText(
|
self._list_model.item(self._port_ids.index(port_id)).setText(
|
||||||
self._ports[port_id].name
|
self._ports[port_id].name
|
||||||
)
|
)
|
||||||
@@ -188,6 +202,7 @@ class PortEditorWidget(QWidget):
|
|||||||
return
|
return
|
||||||
row = self._port_ids.index(port_id)
|
row = self._port_ids.index(port_id)
|
||||||
del self._ports[port_id]
|
del self._ports[port_id]
|
||||||
|
self._port_metadata.pop(port_id, None)
|
||||||
self._port_ids.remove(port_id)
|
self._port_ids.remove(port_id)
|
||||||
selected = (
|
selected = (
|
||||||
self._port_ids[min(row, len(self._port_ids) - 1)]
|
self._port_ids[min(row, len(self._port_ids) - 1)]
|
||||||
@@ -201,6 +216,9 @@ class PortEditorWidget(QWidget):
|
|||||||
signal = self.ui.typeSignal.isChecked()
|
signal = self.ui.typeSignal.isChecked()
|
||||||
self._set_layout_visible(self.ui.signalOptions, signal)
|
self._set_layout_visible(self.ui.signalOptions, signal)
|
||||||
self._set_layout_visible(self.ui.bondOptions, not signal)
|
self._set_layout_visible(self.ui.bondOptions, not signal)
|
||||||
|
annotation_visible = self.ui.multiplicityCheckBox.isChecked()
|
||||||
|
self.ui.connectionAnnotationLabel.setVisible(annotation_visible)
|
||||||
|
self.ui.connectionAnnotationEdit.setVisible(annotation_visible)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _set_layout_visible(layout: QLayout, visible: bool) -> None:
|
def _set_layout_visible(layout: QLayout, visible: bool) -> None:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from copy import deepcopy
|
|||||||
from PySide6.QtCore import QRectF
|
from PySide6.QtCore import QRectF
|
||||||
from PySide6.QtWidgets import QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QSpinBox, QVBoxLayout, QWidget
|
from PySide6.QtWidgets import QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QSpinBox, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bedit_gui.models import Line, LineType, Rectangle, Shape, Text
|
from bedit_gui.models import Ellipse, Line, LineType, Rectangle, Shape, Text
|
||||||
from bedit_gui.views.color_button import ColorButton
|
from bedit_gui.views.color_button import ColorButton
|
||||||
|
|
||||||
|
|
||||||
@@ -34,6 +34,8 @@ class ShapeOptionsDialog(QDialog):
|
|||||||
|
|
||||||
if isinstance(shape, Rectangle):
|
if isinstance(shape, Rectangle):
|
||||||
self._add_rectangle_fields(shape)
|
self._add_rectangle_fields(shape)
|
||||||
|
elif isinstance(shape, Ellipse):
|
||||||
|
self._add_ellipse_fields(shape)
|
||||||
elif isinstance(shape, Text):
|
elif isinstance(shape, Text):
|
||||||
self._add_text_fields(shape)
|
self._add_text_fields(shape)
|
||||||
elif isinstance(shape, Line):
|
elif isinstance(shape, Line):
|
||||||
@@ -51,6 +53,10 @@ class ShapeOptionsDialog(QDialog):
|
|||||||
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
|
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
|
||||||
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
|
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
|
||||||
return Rectangle(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), corner_radius=self.corner_radius.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color())
|
return Rectangle(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), corner_radius=self.corner_radius.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color())
|
||||||
|
if isinstance(self._shape, Ellipse):
|
||||||
|
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
|
||||||
|
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
|
||||||
|
return Ellipse(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color())
|
||||||
if isinstance(self._shape, Text):
|
if isinstance(self._shape, Text):
|
||||||
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
|
width = min(self.width.value(), self._scene_rect.right() - self.x.value())
|
||||||
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
|
height = min(self.height.value(), self._scene_rect.bottom() - self.y.value())
|
||||||
@@ -89,6 +95,29 @@ class ShapeOptionsDialog(QDialog):
|
|||||||
self.form.addRow("Line color", self.line_color)
|
self.form.addRow("Line color", self.line_color)
|
||||||
self.form.addRow("Fill color", self.fill_color)
|
self.form.addRow("Fill color", self.fill_color)
|
||||||
|
|
||||||
|
def _add_ellipse_fields(self, shape: Ellipse) -> None:
|
||||||
|
self.width = QSpinBox()
|
||||||
|
self.width.setRange(1, round(self._scene_rect.width()))
|
||||||
|
self.width.setValue(round(shape.width))
|
||||||
|
self.height = QSpinBox()
|
||||||
|
self.height.setRange(1, round(self._scene_rect.height()))
|
||||||
|
self.height.setValue(round(shape.height))
|
||||||
|
self.line_type = QComboBox()
|
||||||
|
for line_type in LineType:
|
||||||
|
self.line_type.addItem(line_type.value.replace("_", " ").title(), line_type)
|
||||||
|
self.line_type.setCurrentIndex(self.line_type.findData(shape.line_type))
|
||||||
|
self.line_thickness = QDoubleSpinBox()
|
||||||
|
self.line_thickness.setRange(0, 1000)
|
||||||
|
self.line_thickness.setValue(shape.line_thickness)
|
||||||
|
self.line_color = ColorButton(shape.line_color)
|
||||||
|
self.fill_color = ColorButton(shape.fill_color)
|
||||||
|
self.form.addRow("Width", self.width)
|
||||||
|
self.form.addRow("Height", self.height)
|
||||||
|
self.form.addRow("Line type", self.line_type)
|
||||||
|
self.form.addRow("Line thickness", self.line_thickness)
|
||||||
|
self.form.addRow("Line color", self.line_color)
|
||||||
|
self.form.addRow("Fill color", self.fill_color)
|
||||||
|
|
||||||
def _add_text_fields(self, shape: Text) -> None:
|
def _add_text_fields(self, shape: Text) -> None:
|
||||||
self.width = QSpinBox()
|
self.width = QSpinBox()
|
||||||
self.width.setRange(1, round(self._scene_rect.width()))
|
self.width.setRange(1, round(self._scene_rect.width()))
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ class ProcessResult:
|
|||||||
stdout: str
|
stdout: str
|
||||||
stderr: str
|
stderr: str
|
||||||
|
|
||||||
|
def diagnostics(self) -> str:
|
||||||
|
sections = []
|
||||||
|
if self.stdout.strip():
|
||||||
|
sections.append(f"OpenModelica output:\n{self.stdout.strip()}")
|
||||||
|
if self.stderr.strip():
|
||||||
|
sections.append(f"OpenModelica errors:\n{self.stderr.strip()}")
|
||||||
|
return "\n\n".join(sections)
|
||||||
|
|
||||||
|
|
||||||
class OpenModelicaError(RuntimeError):
|
class OpenModelicaError(RuntimeError):
|
||||||
"""Raised when OMC cannot start or reports a failure."""
|
"""Raised when OMC cannot start or reports a failure."""
|
||||||
@@ -80,7 +88,7 @@ class OpenModelicaRunner:
|
|||||||
f"OpenModelica timed out after {error.timeout} seconds"
|
f"OpenModelica timed out after {error.timeout} seconds"
|
||||||
) from error
|
) from error
|
||||||
if result.return_code != 0:
|
if result.return_code != 0:
|
||||||
details = result.stderr.strip() or result.stdout.strip()
|
details = result.diagnostics()
|
||||||
suffix = f": {details}" if details else ""
|
suffix = f": {details}" if details else ""
|
||||||
raise OpenModelicaError(
|
raise OpenModelicaError(
|
||||||
f"OpenModelica exited with status {result.return_code}{suffix}"
|
f"OpenModelica exited with status {result.return_code}{suffix}"
|
||||||
@@ -128,7 +136,7 @@ class OpenModelicaRunner:
|
|||||||
continue
|
continue
|
||||||
result = ProcessResult(command=tuple(command), return_code=process.returncode, stdout=stdout, stderr=stderr)
|
result = ProcessResult(command=tuple(command), return_code=process.returncode, stdout=stdout, stderr=stderr)
|
||||||
if result.return_code != 0:
|
if result.return_code != 0:
|
||||||
details = result.stderr.strip() or result.stdout.strip()
|
details = result.diagnostics()
|
||||||
suffix = f": {details}" if details else ""
|
suffix = f": {details}" if details else ""
|
||||||
raise OpenModelicaError(f"simulation exited with status {result.return_code}{suffix}")
|
raise OpenModelicaError(f"simulation exited with status {result.return_code}{suffix}")
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from .results import SimulationResult, load_openmodelica_csv
|
|||||||
_MODEL_FILE = "model.mo"
|
_MODEL_FILE = "model.mo"
|
||||||
_RUN_SCRIPT_FILE = "run.mos"
|
_RUN_SCRIPT_FILE = "run.mos"
|
||||||
_RUN_OUTPUT_FILE = "simulation-output.txt"
|
_RUN_OUTPUT_FILE = "simulation-output.txt"
|
||||||
|
_DEFAULT_LIBRARIES = ("Modelica",)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -386,7 +387,7 @@ class Simulation:
|
|||||||
if windows_executable.is_file():
|
if windows_executable.is_file():
|
||||||
executable = windows_executable
|
executable = windows_executable
|
||||||
else:
|
else:
|
||||||
details = process.stderr.strip() or process.stdout.strip()
|
details = process.diagnostics()
|
||||||
suffix = f": {details}" if details else ""
|
suffix = f": {details}" if details else ""
|
||||||
raise OpenModelicaError(
|
raise OpenModelicaError(
|
||||||
f"OpenModelica did not build {model_name!r}{suffix}"
|
f"OpenModelica did not build {model_name!r}{suffix}"
|
||||||
@@ -692,10 +693,20 @@ def _load_model_script(
|
|||||||
working_directory: Path,
|
working_directory: Path,
|
||||||
commands: Sequence[str],
|
commands: Sequence[str],
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create a script that loads ``model.mo`` before custom commands."""
|
"""Create a script that loads default libraries and ``model.mo`` before custom commands."""
|
||||||
|
load_libraries = []
|
||||||
|
for library in _DEFAULT_LIBRARIES:
|
||||||
|
load_libraries.extend([
|
||||||
|
f"loaded := loadModel({library});",
|
||||||
|
"if not loaded then",
|
||||||
|
" print(getErrorString());",
|
||||||
|
" exit(1);",
|
||||||
|
"end if;",
|
||||||
|
])
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
[
|
[
|
||||||
f"cd({json.dumps(str(working_directory.resolve()))});",
|
f"cd({json.dumps(str(working_directory.resolve()))});",
|
||||||
|
*load_libraries,
|
||||||
f"loaded := loadFile({json.dumps(_MODEL_FILE)});",
|
f"loaded := loadFile({json.dumps(_MODEL_FILE)});",
|
||||||
"if not loaded then",
|
"if not loaded then",
|
||||||
" print(getErrorString());",
|
" print(getErrorString());",
|
||||||
|
|||||||
@@ -607,6 +607,387 @@
|
|||||||
"end for;"
|
"end for;"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"404e82f4-19e2-44ad-b409-3f021e483a39": {
|
||||||
|
"name": "integrator",
|
||||||
|
"interface": {
|
||||||
|
"ports": {
|
||||||
|
"441a44d0-bbe5-49f0-bb0e-5bc891218aca": {
|
||||||
|
"port_type": "signal",
|
||||||
|
"name": "y",
|
||||||
|
"direction": "output",
|
||||||
|
"multiplicity": false,
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"description": null,
|
||||||
|
"value_type": "real",
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null
|
||||||
|
},
|
||||||
|
"8d4b0f2c-2e13-4cfd-a8da-6c03ade4fbbf": {
|
||||||
|
"port_type": "signal",
|
||||||
|
"name": "u",
|
||||||
|
"direction": "input",
|
||||||
|
"multiplicity": false,
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"description": null,
|
||||||
|
"value_type": "real",
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"7150c82e-8c78-4f83-9bc8-e46057a367a8": {
|
||||||
|
"name": "k",
|
||||||
|
"value": "1.0",
|
||||||
|
"value_type": "real",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": "Integrator gain"
|
||||||
|
},
|
||||||
|
"f7b9691a-22a4-41e0-9871-44f8dd0e02d2": {
|
||||||
|
"name": "initType",
|
||||||
|
"value": "3",
|
||||||
|
"value_type": "int",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": "Type of initialization (1: no init, 2: steady state, 3: initial state, 4: initial output)"
|
||||||
|
},
|
||||||
|
"3b636a9c-257c-4368-855a-094051f29041": {
|
||||||
|
"name": "y_start",
|
||||||
|
"value": "0.0",
|
||||||
|
"value_type": "real",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": "Initial or guess value of output (= state)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"implementation_type": "equation",
|
||||||
|
"declarations": [],
|
||||||
|
"initial_equations": [
|
||||||
|
"if initType == 2 then",
|
||||||
|
"\tder(y) = 0;",
|
||||||
|
"elseif initType >= 3 then",
|
||||||
|
"\ty = y_start;",
|
||||||
|
"end if;"
|
||||||
|
],
|
||||||
|
"equations": [
|
||||||
|
"der(y) = k*u;"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2b0156f6-9977-4735-88dd-14f5eb3d87b5": {
|
||||||
|
"name": "derivative",
|
||||||
|
"interface": {
|
||||||
|
"ports": {
|
||||||
|
"d61847c9-0628-4e86-ad91-9390f92f926d": {
|
||||||
|
"port_type": "signal",
|
||||||
|
"name": "y",
|
||||||
|
"direction": "output",
|
||||||
|
"multiplicity": false,
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"description": null,
|
||||||
|
"value_type": "real",
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null
|
||||||
|
},
|
||||||
|
"f18c8fd2-dda5-4e8d-a5a7-a22a38abb091": {
|
||||||
|
"port_type": "signal",
|
||||||
|
"name": "u",
|
||||||
|
"direction": "input",
|
||||||
|
"multiplicity": false,
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"description": null,
|
||||||
|
"value_type": "real",
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"ec421126-1ff4-4627-8bdd-1841318f3eb6": {
|
||||||
|
"name": "k",
|
||||||
|
"value": "1.0",
|
||||||
|
"value_type": "real",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": "Integrator gain"
|
||||||
|
},
|
||||||
|
"0c364769-5f07-4f3e-bb93-91d86a1f2dae": {
|
||||||
|
"name": "initType",
|
||||||
|
"value": "3",
|
||||||
|
"value_type": "int",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": "Type of initialization (1: no init, 2: steady state, 3: initial state, 4: initial output)"
|
||||||
|
},
|
||||||
|
"ab3a2bae-ea7f-41ca-9493-93d8debcee27": {
|
||||||
|
"name": "y_start",
|
||||||
|
"value": "0.0",
|
||||||
|
"value_type": "real",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": "Initial or guess value of output (= state)"
|
||||||
|
},
|
||||||
|
"e04d9fc5-761c-4b3d-bd7b-93da9896bd35": {
|
||||||
|
"name": "T",
|
||||||
|
"value": "0.01",
|
||||||
|
"value_type": "real",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": "Time constants (T>0 required; T=0 is ideal derivative block)"
|
||||||
|
},
|
||||||
|
"29669830-2738-4692-8255-115d1546619c": {
|
||||||
|
"name": "x_start",
|
||||||
|
"value": "0.0",
|
||||||
|
"value_type": "real",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": "Initial or guess value of state\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"implementation_type": "equation",
|
||||||
|
"declarations": [
|
||||||
|
"parameter Boolean zeroGain = abs(k) < Modelica.Constants.eps;",
|
||||||
|
"Real x(start=x_start);"
|
||||||
|
],
|
||||||
|
"initial_equations": [
|
||||||
|
"if initType == 2 then",
|
||||||
|
"\tder(x) = 0;",
|
||||||
|
"elseif initType == 3 then",
|
||||||
|
"\tx = x_start;",
|
||||||
|
"elseif initType >= 4 then",
|
||||||
|
"\tif zeroGain then",
|
||||||
|
"\t\tx = u;",
|
||||||
|
"\telse",
|
||||||
|
"\t\ty = y_start;",
|
||||||
|
"\tend if;",
|
||||||
|
"end if;"
|
||||||
|
],
|
||||||
|
"equations": [
|
||||||
|
"der(x) = if zeroGain then 0 else (u-x)/T;",
|
||||||
|
"y = if zeroGain then 0 else (k/T)*(u-x);"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"927326ff-f73a-4f98-b7fd-aaf97989121e": {
|
||||||
|
"name": "pulse",
|
||||||
|
"interface": {
|
||||||
|
"ports": {
|
||||||
|
"4f9f6b29-36f0-460e-9fd9-d4bdf0ea3c80": {
|
||||||
|
"port_type": "signal",
|
||||||
|
"name": "y",
|
||||||
|
"direction": "output",
|
||||||
|
"multiplicity": false,
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"description": null,
|
||||||
|
"value_type": "real",
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"b6460a26-0b89-49e0-8ec7-99281a66668b": {
|
||||||
|
"name": "amplitude",
|
||||||
|
"value": "1.0",
|
||||||
|
"value_type": "real",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
"983d6c65-8e81-4f49-9faa-d024e8e6a9d3": {
|
||||||
|
"name": "width",
|
||||||
|
"value": "50.0",
|
||||||
|
"value_type": "real",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": "Pulse width in %"
|
||||||
|
},
|
||||||
|
"c9ccd873-f83a-4b08-8827-4a88a06d502a": {
|
||||||
|
"name": "offset",
|
||||||
|
"value": "0",
|
||||||
|
"value_type": "real",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
"9cd46f38-6ddb-4d69-946d-df96e9f30078": {
|
||||||
|
"name": "period",
|
||||||
|
"value": "1.0",
|
||||||
|
"value_type": "real",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
"4e1d29eb-48c8-479a-8968-8f57fd95ea9f": {
|
||||||
|
"name": "nperiod",
|
||||||
|
"value": "-1",
|
||||||
|
"value_type": "int",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": "Number of periods (<0 means infinite)"
|
||||||
|
},
|
||||||
|
"68102f0d-b58b-4c10-af0d-dffae406fb07": {
|
||||||
|
"name": "startTime",
|
||||||
|
"value": "0.0",
|
||||||
|
"value_type": "real",
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null,
|
||||||
|
"description": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"implementation_type": "equation",
|
||||||
|
"declarations": [
|
||||||
|
"Integer count;",
|
||||||
|
"Modelica.Units.SI.Time T_start;",
|
||||||
|
"Modelica.Units.SI.Time T_width=period*width/100;"
|
||||||
|
],
|
||||||
|
"initial_equations": [
|
||||||
|
"initial algorithm",
|
||||||
|
"count := integer((time-startTime)/period);",
|
||||||
|
"T_start := startTime + count*period;"
|
||||||
|
],
|
||||||
|
"equations": [
|
||||||
|
"when time >= (pre(count)+1)*period + startTime then",
|
||||||
|
"\tcount = pre(count) + 1;",
|
||||||
|
"\tT_start = time;",
|
||||||
|
"end when;",
|
||||||
|
"y = offset + (if (time < startTime or nperiod == 0 or (nperiod > 0 and count >= nperiod)) then 0 else if time < T_start + T_width then amplitude else 0);"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ac215607-dd97-4142-b42f-f23502e51cfa": {
|
||||||
|
"name": "plusminus",
|
||||||
|
"interface": {
|
||||||
|
"ports": {
|
||||||
|
"baadcb27-0807-4366-819f-7396a1ed205d": {
|
||||||
|
"port_type": "signal",
|
||||||
|
"name": "plus",
|
||||||
|
"direction": "input",
|
||||||
|
"multiplicity": true,
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"description": null,
|
||||||
|
"value_type": "real",
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null
|
||||||
|
},
|
||||||
|
"53cc98de-2e05-49ea-af26-e26d5dcd99e7": {
|
||||||
|
"port_type": "signal",
|
||||||
|
"name": "minus",
|
||||||
|
"direction": "input",
|
||||||
|
"multiplicity": true,
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"description": null,
|
||||||
|
"value_type": "real",
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null
|
||||||
|
},
|
||||||
|
"0574652d-df52-4798-8072-da0a26eee873": {
|
||||||
|
"port_type": "signal",
|
||||||
|
"name": "y",
|
||||||
|
"direction": "output",
|
||||||
|
"multiplicity": false,
|
||||||
|
"matrix_size": [
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"description": null,
|
||||||
|
"value_type": "real",
|
||||||
|
"quantity": null,
|
||||||
|
"unit": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": {},
|
||||||
|
"implementation": {
|
||||||
|
"implementation_type": "equation",
|
||||||
|
"declarations": [],
|
||||||
|
"initial_equations": [],
|
||||||
|
"equations": [
|
||||||
|
"y = sum(plus) - sum(minus);"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"connections": {
|
"connections": {
|
||||||
@@ -661,6 +1042,26 @@
|
|||||||
"connection_type": "signal",
|
"connection_type": "signal",
|
||||||
"source": "a2709c23-7485-43b2-bc87-28a0f4c37c0b",
|
"source": "a2709c23-7485-43b2-bc87-28a0f4c37c0b",
|
||||||
"target": "8bd1cc42-03b9-4c46-b31b-c1f46933dc9e"
|
"target": "8bd1cc42-03b9-4c46-b31b-c1f46933dc9e"
|
||||||
|
},
|
||||||
|
"122eaf51-7baf-457f-a944-f946b6ab321a": {
|
||||||
|
"connection_type": "signal",
|
||||||
|
"source": "4f9f6b29-36f0-460e-9fd9-d4bdf0ea3c80",
|
||||||
|
"target": "baadcb27-0807-4366-819f-7396a1ed205d"
|
||||||
|
},
|
||||||
|
"8b1c34a9-03c2-4bb5-81ab-7fa66bcafe8e": {
|
||||||
|
"connection_type": "signal",
|
||||||
|
"source": "5807dc83-d049-4f0f-9708-d041a752fcd0",
|
||||||
|
"target": "53cc98de-2e05-49ea-af26-e26d5dcd99e7"
|
||||||
|
},
|
||||||
|
"3c659d9e-8218-48ac-9a3d-4297fba275c2": {
|
||||||
|
"connection_type": "signal",
|
||||||
|
"source": "0574652d-df52-4798-8072-da0a26eee873",
|
||||||
|
"target": "8d4b0f2c-2e13-4cfd-a8da-6c03ade4fbbf"
|
||||||
|
},
|
||||||
|
"9b5ab2d7-d302-48e8-98dc-0d60ba54ca01": {
|
||||||
|
"connection_type": "signal",
|
||||||
|
"source": "0574652d-df52-4798-8072-da0a26eee873",
|
||||||
|
"target": "f18c8fd2-dda5-4e8d-a5a7-a22a38abb091"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1149,6 +1550,286 @@
|
|||||||
-32
|
-32
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"404e82f4-19e2-44ad-b409-3f021e483a39": {
|
||||||
|
"shapes": {
|
||||||
|
"29dafe3d-8392-49c0-98ee-80e941b32a52": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "rectangle",
|
||||||
|
"pos": [
|
||||||
|
-48,
|
||||||
|
-48
|
||||||
|
],
|
||||||
|
"width": 96.0,
|
||||||
|
"height": 96.0,
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 5.0,
|
||||||
|
"corner_radius": 8.0,
|
||||||
|
"line_color": "#00007fff",
|
||||||
|
"fill_color": "#ebebebff"
|
||||||
|
},
|
||||||
|
"d24da1c9-bae9-41e6-aee4-79e5f238ea72": {
|
||||||
|
"layer": 1,
|
||||||
|
"type": "text",
|
||||||
|
"pos": [
|
||||||
|
-15,
|
||||||
|
-47
|
||||||
|
],
|
||||||
|
"width": 31.0,
|
||||||
|
"height": 81.0,
|
||||||
|
"color": "#00007fff",
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"size": 72.0,
|
||||||
|
"text": "∫"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"441a44d0-bbe5-49f0-bb0e-5bc891218aca": [
|
||||||
|
7,
|
||||||
|
-7
|
||||||
|
],
|
||||||
|
"8d4b0f2c-2e13-4cfd-a8da-6c03ade4fbbf": [
|
||||||
|
-22,
|
||||||
|
-7
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2b0156f6-9977-4735-88dd-14f5eb3d87b5": {
|
||||||
|
"shapes": {
|
||||||
|
"702ce5c1-a37a-4f89-8261-0be4b433ae79": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "rectangle",
|
||||||
|
"pos": [
|
||||||
|
-48,
|
||||||
|
-48
|
||||||
|
],
|
||||||
|
"width": 96.0,
|
||||||
|
"height": 96.0,
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 5.0,
|
||||||
|
"corner_radius": 8.0,
|
||||||
|
"line_color": "#00007fff",
|
||||||
|
"fill_color": "#ebebebff"
|
||||||
|
},
|
||||||
|
"32ccfd50-07dd-4524-be5d-e96a3136d520": {
|
||||||
|
"layer": 1,
|
||||||
|
"type": "text",
|
||||||
|
"pos": [
|
||||||
|
-40,
|
||||||
|
-31
|
||||||
|
],
|
||||||
|
"width": 78.0,
|
||||||
|
"height": 62.0,
|
||||||
|
"color": "#00007fff",
|
||||||
|
"bold": false,
|
||||||
|
"italic": false,
|
||||||
|
"size": 38.0,
|
||||||
|
"text": "d/dt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"d61847c9-0628-4e86-ad91-9390f92f926d": [
|
||||||
|
7,
|
||||||
|
-7
|
||||||
|
],
|
||||||
|
"f18c8fd2-dda5-4e8d-a5a7-a22a38abb091": [
|
||||||
|
-22,
|
||||||
|
-7
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"927326ff-f73a-4f98-b7fd-aaf97989121e": {
|
||||||
|
"shapes": {
|
||||||
|
"89e11690-3faa-482b-a054-10a2c7565c43": {
|
||||||
|
"layer": 0,
|
||||||
|
"type": "rectangle",
|
||||||
|
"pos": [
|
||||||
|
-48,
|
||||||
|
-48
|
||||||
|
],
|
||||||
|
"width": 96.0,
|
||||||
|
"height": 96.0,
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 5.0,
|
||||||
|
"corner_radius": 8.0,
|
||||||
|
"line_color": "#00007fff",
|
||||||
|
"fill_color": "#ebebebff"
|
||||||
|
},
|
||||||
|
"de170a5b-c959-4d6d-afda-fa867dfbca51": {
|
||||||
|
"layer": 4,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
-32
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
-32,
|
||||||
|
32
|
||||||
|
],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 3.0,
|
||||||
|
"line_color": "#000000ff"
|
||||||
|
},
|
||||||
|
"94fc1429-0ef2-403b-aa5d-7edcb7b3ebf2": {
|
||||||
|
"layer": 2,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
32
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
32,
|
||||||
|
32
|
||||||
|
],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 3.0,
|
||||||
|
"line_color": "#000000ff"
|
||||||
|
},
|
||||||
|
"0d82b577-e3fb-4434-adfb-883e1918e30f": {
|
||||||
|
"layer": 3,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [
|
||||||
|
-32,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
-14,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 2.0,
|
||||||
|
"line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"531dcbea-2c2d-4ed9-bcbc-dfc9b0145527": {
|
||||||
|
"layer": 5,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [
|
||||||
|
-13,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
-13,
|
||||||
|
-12
|
||||||
|
],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 2.0,
|
||||||
|
"line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"c4476a1b-7216-4eab-aad6-69ecb5af4b79": {
|
||||||
|
"layer": 6,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [
|
||||||
|
-13,
|
||||||
|
-13
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
4,
|
||||||
|
-13
|
||||||
|
],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 2.0,
|
||||||
|
"line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"2b69a1e2-0f80-4a1a-b31c-1f32b91bc46a": {
|
||||||
|
"layer": 7,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [
|
||||||
|
4,
|
||||||
|
-13
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
4,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 2.0,
|
||||||
|
"line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"7fac6744-2d5b-49a4-bfca-27cc8889a5b6": {
|
||||||
|
"layer": 8,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [
|
||||||
|
4,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
21,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 2.0,
|
||||||
|
"line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"81390b87-3648-4556-a439-9456232781df": {
|
||||||
|
"layer": 9,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [
|
||||||
|
21,
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
21,
|
||||||
|
-13
|
||||||
|
],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 2.0,
|
||||||
|
"line_color": "#ffaa00ff"
|
||||||
|
},
|
||||||
|
"c3c9eddf-f4c7-4f76-8bc5-7c313fc9ea72": {
|
||||||
|
"layer": 10,
|
||||||
|
"type": "line",
|
||||||
|
"pos": [
|
||||||
|
21,
|
||||||
|
-14
|
||||||
|
],
|
||||||
|
"end": [
|
||||||
|
32,
|
||||||
|
-14
|
||||||
|
],
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 2.0,
|
||||||
|
"line_color": "#ffaa00ff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"4f9f6b29-36f0-460e-9fd9-d4bdf0ea3c80": [
|
||||||
|
-8,
|
||||||
|
-8
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ac215607-dd97-4142-b42f-f23502e51cfa": {
|
||||||
|
"shapes": {
|
||||||
|
"9336da30-7335-4eea-a8a2-ae69d8cad9e1": {
|
||||||
|
"layer": 1,
|
||||||
|
"type": "ellipse",
|
||||||
|
"pos": [
|
||||||
|
-36,
|
||||||
|
-36
|
||||||
|
],
|
||||||
|
"width": 72.0,
|
||||||
|
"height": 72.0,
|
||||||
|
"line_type": "solid",
|
||||||
|
"line_thickness": 8.0,
|
||||||
|
"line_color": "#00007fff",
|
||||||
|
"fill_color": "#ebebebff"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"port_positions": {
|
||||||
|
"baadcb27-0807-4366-819f-7396a1ed205d": [
|
||||||
|
-24,
|
||||||
|
9
|
||||||
|
],
|
||||||
|
"53cc98de-2e05-49ea-af26-e26d5dcd99e7": [
|
||||||
|
-24,
|
||||||
|
-26
|
||||||
|
],
|
||||||
|
"0574652d-df52-4798-8072-da0a26eee873": [
|
||||||
|
11,
|
||||||
|
-8
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1207,8 +1888,8 @@
|
|||||||
48
|
48
|
||||||
],
|
],
|
||||||
"2b3844e8-1875-40d0-ae4e-7a14ca217bce": [
|
"2b3844e8-1875-40d0-ae4e-7a14ca217bce": [
|
||||||
-432,
|
-428,
|
||||||
236
|
744
|
||||||
],
|
],
|
||||||
"bf08f999-bfe8-493a-9ad2-9c2965e17d6d": [
|
"bf08f999-bfe8-493a-9ad2-9c2965e17d6d": [
|
||||||
-504,
|
-504,
|
||||||
@@ -1233,6 +1914,38 @@
|
|||||||
"8e8dca73-2d11-4631-9c24-5e1a4e3b2920": [
|
"8e8dca73-2d11-4631-9c24-5e1a4e3b2920": [
|
||||||
64,
|
64,
|
||||||
128
|
128
|
||||||
|
],
|
||||||
|
"ac5011ce-4659-4bdd-900a-63a1c779652c": [
|
||||||
|
-432,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"ec0599cb-b57b-4194-b166-526d5cd5382a": [
|
||||||
|
-120,
|
||||||
|
656
|
||||||
|
],
|
||||||
|
"83dd8220-554c-4907-b713-c37e1f600f0d": [
|
||||||
|
-96,
|
||||||
|
648
|
||||||
|
],
|
||||||
|
"404e82f4-19e2-44ad-b409-3f021e483a39": [
|
||||||
|
228,
|
||||||
|
640
|
||||||
|
],
|
||||||
|
"2b0156f6-9977-4735-88dd-14f5eb3d87b5": [
|
||||||
|
232,
|
||||||
|
828
|
||||||
|
],
|
||||||
|
"927326ff-f73a-4f98-b7fd-aaf97989121e": [
|
||||||
|
-428,
|
||||||
|
552
|
||||||
|
],
|
||||||
|
"ac215607-dd97-4142-b42f-f23502e51cfa": [
|
||||||
|
-96,
|
||||||
|
636
|
||||||
|
],
|
||||||
|
"b3e059a3-4517-435d-aa63-ddfbbf6fa071": [
|
||||||
|
488,
|
||||||
|
552
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"component_labels": {
|
"component_labels": {
|
||||||
@@ -1343,6 +2056,17 @@
|
|||||||
"dassl_tolerance": 1e-06
|
"dassl_tolerance": 1e-06
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"port_metadata_database": {
|
||||||
|
"format_version": 1,
|
||||||
|
"ports": {
|
||||||
|
"53cc98de-2e05-49ea-af26-e26d5dcd99e7": {
|
||||||
|
"connection_annotation": "-"
|
||||||
|
},
|
||||||
|
"baadcb27-0807-4366-819f-7396a1ed205d": {
|
||||||
|
"connection_annotation": "+"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user