Start with causality inference
This commit is contained in:
@@ -56,8 +56,9 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
|
|||||||
anchors may change without changing IDs.
|
anchors may change without changing IDs.
|
||||||
- Parameters belong to `Component` rather than a particular implementation kind,
|
- Parameters belong to `Component` rather than a particular implementation kind,
|
||||||
so graph and text components share stable-ID name/type/value records.
|
so graph and text components share stable-ID name/type/value records.
|
||||||
- Port orientation is presented as one unified list in the UI, while the model
|
- Components store one ordered `ports` list. Each port's `orientation` field is
|
||||||
indexes inputs and outputs separately for connection semantics.
|
authoritative (`input`, `output`, or power-only `indifferent`); connection and
|
||||||
|
presentation code derives any directional groupings when needed.
|
||||||
- Port types are registered in `core/port_types.py`. Only compatible types may be
|
- Port types are registered in `core/port_types.py`. Only compatible types may be
|
||||||
connected. Signal ports connect by type; power ports additionally require the
|
connected. Signal ports connect by type; power ports additionally require the
|
||||||
same domain. Editable power domains and causalities live in
|
same domain. Editable power domains and causalities live in
|
||||||
@@ -72,9 +73,15 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
|
|||||||
- Port removal or reorientation must be rejected when it would invalidate an
|
- Port removal or reorientation must be rejected when it would invalidate an
|
||||||
existing connection.
|
existing connection.
|
||||||
- Connections reference port IDs, never port names.
|
- Connections reference port IDs, never port names.
|
||||||
|
- Connections store their port type and bond-graph causality explicitly.
|
||||||
|
`core/bond_graph.py` owns causality inference; the simulation service invokes
|
||||||
|
it before Modelica composition. The controller copies inferred values into the
|
||||||
|
live model and refreshes the workspace before compile, export, or run.
|
||||||
|
Causality values are `none`, `source`, `target`, `warn_source`, and `warn_target`.
|
||||||
- Connection junctions are explicit typed graph objects. Splitting a connection
|
- Connection junctions are explicit typed graph objects. Splitting a connection
|
||||||
creates one incoming and one outgoing segment; the junction can source further
|
creates one incoming and one outgoing segment; the junction can source further
|
||||||
branches without overlapping full connection paths.
|
branches without overlapping full connection paths. Power bond connections
|
||||||
|
cannot contain junctions.
|
||||||
- Graph interaction has separate Pointer and Connect modes. Port hints are only
|
- Graph interaction has separate Pointer and Connect modes. Port hints are only
|
||||||
visible in Connect mode. Connecting two blocks opens the compatible port-pair
|
visible in Connect mode. Connecting two blocks opens the compatible port-pair
|
||||||
chooser; explicit port clicks determine its default selection. Connections
|
chooser; explicit port clicks determine its default selection. Connections
|
||||||
@@ -324,6 +331,7 @@ PYTHONPATH=src python3 -m bedit
|
|||||||
```
|
```
|
||||||
|
|
||||||
The installed GUI entry point is `bedit.gui.app:main`.
|
The installed GUI entry point is `bedit.gui.app:main`.
|
||||||
|
The first non-flag launch argument is opened as the initial document.
|
||||||
|
|
||||||
## Completion checklist
|
## Completion checklist
|
||||||
|
|
||||||
|
|||||||
@@ -159,12 +159,12 @@ Every component owns its ports, declarative icon, properties, and child graph:
|
|||||||
"name": "My Component",
|
"name": "My Component",
|
||||||
"position": {"x": 0, "y": 0},
|
"position": {"x": 0, "y": 0},
|
||||||
"interface": {
|
"interface": {
|
||||||
"inputs": [{"id": "in", "name": "Input", "type": "signal", "properties": {
|
"ports": [
|
||||||
"iconPosition": {"x": 0, "y": 40}
|
{"id": "in", "name": "Input", "type": "signal", "orientation": "input",
|
||||||
}}],
|
"properties": {"iconPosition": {"x": 0, "y": 40}}},
|
||||||
"outputs": [{"id": "out", "name": "Output", "type": "signal", "properties": {
|
{"id": "out", "name": "Output", "type": "signal", "orientation": "output",
|
||||||
"iconPosition": {"x": 128, "y": 64}
|
"properties": {"iconPosition": {"x": 128, "y": 64}}}
|
||||||
}}]
|
]
|
||||||
},
|
},
|
||||||
"icon": {
|
"icon": {
|
||||||
"size": {"width": 128, "height": 128},
|
"size": {"width": 128, "height": 128},
|
||||||
|
|||||||
134
BEdit/src/bedit/core/bond_graph.py
Normal file
134
BEdit/src/bedit/core/bond_graph.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
"""Bond-graph analysis hooks.
|
||||||
|
|
||||||
|
This module intentionally has no GUI or simulation-engine dependencies. The
|
||||||
|
causality inference algorithm can grow here without coupling the document model
|
||||||
|
to OpenModelica or Qt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from bedit.core.application_log import get_logger
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
def infer_causality(component: dict[str, Any], toplevel: bool = True) -> dict[str, Any]:
|
||||||
|
"""Infer power-connection causality in a serialized component tree.
|
||||||
|
|
||||||
|
This is currently a traversal stub: it ensures every power connection has a
|
||||||
|
causality value, while preserving causality already supplied by callers.
|
||||||
|
Future inference rules should assign ``source``, ``target``,
|
||||||
|
``warn_source``, or ``warn_target`` here and return the same tree.
|
||||||
|
|
||||||
|
The input is mutated and returned so the composer receives the inferred
|
||||||
|
representation without needing a second document conversion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if toplevel:
|
||||||
|
log.info("Causality inference")
|
||||||
|
|
||||||
|
# Reset causalities
|
||||||
|
reset_causality(component)
|
||||||
|
|
||||||
|
implementation = component.get("implementation", {})
|
||||||
|
graph = implementation.get("graph") if isinstance(implementation, dict) else None
|
||||||
|
if not isinstance(graph, dict):
|
||||||
|
return component
|
||||||
|
|
||||||
|
id_list = build_id_list(component)
|
||||||
|
|
||||||
|
# Check fixed causality
|
||||||
|
for connection in graph.get("connections", []):
|
||||||
|
if isinstance(connection, dict) and connection.get("type") == "power":
|
||||||
|
source, target = get_ports_from_bond(connection, id_list)
|
||||||
|
log.info(source)
|
||||||
|
log.info(target)
|
||||||
|
if source.get('causality', 'indifferent') == 'fixed effort out':
|
||||||
|
connection['causality'] = 'target'
|
||||||
|
elif source.get('causality', 'indifferent') == 'fixed flow out':
|
||||||
|
connection['causality'] = 'source'
|
||||||
|
|
||||||
|
for block in graph.get("blocks", []):
|
||||||
|
if isinstance(block, dict):
|
||||||
|
infer_causality(block, False)
|
||||||
|
|
||||||
|
return component
|
||||||
|
|
||||||
|
def reset_causality(component: dict[str, Any]) -> None:
|
||||||
|
implementation = component.get("implementation", {})
|
||||||
|
graph = implementation.get("graph") if isinstance(implementation, dict) else None
|
||||||
|
if not isinstance(graph, dict):
|
||||||
|
return
|
||||||
|
for connection in graph.get("connections", []):
|
||||||
|
if isinstance(connection, dict) and connection.get("type") == "power":
|
||||||
|
connection["causality"] = "none"
|
||||||
|
for block in graph.get("blocks", []):
|
||||||
|
if isinstance(block, dict):
|
||||||
|
reset_causality(block)
|
||||||
|
|
||||||
|
def build_id_list(graph: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Index all addressable objects in a component tree by their stable ID."""
|
||||||
|
|
||||||
|
id_list: dict[str, Any] = {}
|
||||||
|
id_kinds: dict[str, str] = {}
|
||||||
|
|
||||||
|
def add(item: dict[str, Any], description: str) -> None:
|
||||||
|
item_id = item.get("id")
|
||||||
|
if not item_id:
|
||||||
|
raise ValueError(f"{description} has no ID")
|
||||||
|
# Port IDs identify a port on a component definition and may therefore
|
||||||
|
# recur in cloned component instances. Component and junction IDs are
|
||||||
|
# document objects and must remain globally unique.
|
||||||
|
if item_id in id_list and not (
|
||||||
|
description == "port" and id_kinds[item_id] == "port"
|
||||||
|
):
|
||||||
|
raise ValueError(f"Duplicate simulation object ID: {item_id}")
|
||||||
|
id_list[item_id] = item
|
||||||
|
id_kinds[item_id] = description
|
||||||
|
|
||||||
|
def visit(component: dict[str, Any]) -> None:
|
||||||
|
add(component, "component")
|
||||||
|
interface = component.get("interface", {})
|
||||||
|
for port in interface.get("ports", []):
|
||||||
|
add(port, "port")
|
||||||
|
|
||||||
|
implementation = component.get("implementation", {})
|
||||||
|
if implementation.get("kind") != "graph":
|
||||||
|
return
|
||||||
|
nested_graph = implementation.get("graph", {})
|
||||||
|
for junction in nested_graph.get("junctions", []):
|
||||||
|
add(junction, "junction")
|
||||||
|
for block in nested_graph.get("blocks", []):
|
||||||
|
visit(block)
|
||||||
|
|
||||||
|
visit(graph)
|
||||||
|
return id_list
|
||||||
|
|
||||||
|
def get_ports_from_bond(bond: dict[str, Any], id_list: dict[str, Any]) -> tuple[dict[str,Any], dict[str,Any]]:
|
||||||
|
source = bond.get('source', None)
|
||||||
|
target = bond.get('target', None)
|
||||||
|
if source is None or target is None:
|
||||||
|
raise ValueError("Source and Target of a power bond cannot be None")
|
||||||
|
|
||||||
|
source_component = id_list.get(source.get('block'), None)
|
||||||
|
target_component = id_list.get(target.get('block'), None)
|
||||||
|
if source_component is None or target_component is None:
|
||||||
|
raise ValueError("Source or Target blocks not found")
|
||||||
|
|
||||||
|
def _find_port(ports: list[dict[str, Any]], port: str) -> dict[str, Any] | None:
|
||||||
|
for pi in ports:
|
||||||
|
if pi.get('id', '') == port:
|
||||||
|
return pi
|
||||||
|
return None
|
||||||
|
|
||||||
|
source_port = _find_port(source_component.get('interface', {}).get('ports', []), source.get('port'))
|
||||||
|
target_port = _find_port(target_component.get('interface', {}).get('ports', []), target.get('port'))
|
||||||
|
if source_port is None or target_port is None:
|
||||||
|
raise ValueError("Source or Target port not found")
|
||||||
|
|
||||||
|
if source_port.get('type') != 'power':
|
||||||
|
raise ValueError("Source port is not a power port")
|
||||||
|
if target_port.get('type') != 'power':
|
||||||
|
raise ValueError("Target port is not a power port")
|
||||||
|
|
||||||
|
return source_port, target_port
|
||||||
@@ -180,6 +180,8 @@ class Connection:
|
|||||||
target: Endpoint
|
target: Endpoint
|
||||||
name: str = ""
|
name: str = ""
|
||||||
properties: dict[str, Any] = field(default_factory=dict)
|
properties: dict[str, Any] = field(default_factory=dict)
|
||||||
|
type: str = "signal"
|
||||||
|
causality: str = "none"
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
@@ -188,6 +190,8 @@ class Connection:
|
|||||||
"target": self.target.to_dict(),
|
"target": self.target.to_dict(),
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"properties": self.properties,
|
"properties": self.properties,
|
||||||
|
"type": self.type,
|
||||||
|
"causality": self.causality,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -198,6 +202,8 @@ class Connection:
|
|||||||
target=Endpoint.from_dict(data["target"]),
|
target=Endpoint.from_dict(data["target"]),
|
||||||
name=str(data.get("name", "")),
|
name=str(data.get("name", "")),
|
||||||
properties=dict(data.get("properties", {})),
|
properties=dict(data.get("properties", {})),
|
||||||
|
type=str(data.get("type", "signal")),
|
||||||
|
causality=str(data.get("causality", "none")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -360,8 +366,7 @@ class Component:
|
|||||||
x: float = 0.0
|
x: float = 0.0
|
||||||
y: float = 0.0
|
y: float = 0.0
|
||||||
rotation: float = 0.0
|
rotation: float = 0.0
|
||||||
inputs: list[Port] = field(default_factory=list)
|
ports: list[Port] = field(default_factory=list)
|
||||||
outputs: list[Port] = field(default_factory=list)
|
|
||||||
parameters: list[Parameter] = field(default_factory=list)
|
parameters: list[Parameter] = field(default_factory=list)
|
||||||
icon: Icon = field(default_factory=Icon)
|
icon: Icon = field(default_factory=Icon)
|
||||||
properties: dict[str, Any] = field(default_factory=dict)
|
properties: dict[str, Any] = field(default_factory=dict)
|
||||||
@@ -381,10 +386,7 @@ class Component:
|
|||||||
"name": self.name,
|
"name": self.name,
|
||||||
"position": {"x": self.x, "y": self.y},
|
"position": {"x": self.x, "y": self.y},
|
||||||
"rotation": self.rotation,
|
"rotation": self.rotation,
|
||||||
"interface": {
|
"interface": {"ports": [port.to_dict() for port in self.ports]},
|
||||||
"inputs": [port.to_dict() for port in self.inputs],
|
|
||||||
"outputs": [port.to_dict() for port in self.outputs],
|
|
||||||
},
|
|
||||||
"parameters": [parameter.to_dict() for parameter in self.parameters],
|
"parameters": [parameter.to_dict() for parameter in self.parameters],
|
||||||
"icon": self.icon.to_dict(),
|
"icon": self.icon.to_dict(),
|
||||||
"properties": self.properties,
|
"properties": self.properties,
|
||||||
@@ -427,8 +429,7 @@ class Component:
|
|||||||
x=float(position.get("x", 0.0)),
|
x=float(position.get("x", 0.0)),
|
||||||
y=float(position.get("y", 0.0)),
|
y=float(position.get("y", 0.0)),
|
||||||
rotation=float(data.get("rotation", 0.0)),
|
rotation=float(data.get("rotation", 0.0)),
|
||||||
inputs=[Port.from_dict(item, "input") for item in interface.get("inputs", [])],
|
ports=[Port.from_dict(item) for item in interface.get("ports", [])],
|
||||||
outputs=[Port.from_dict(item, "output") for item in interface.get("outputs", [])],
|
|
||||||
parameters=[Parameter.from_dict(item) for item in parameters],
|
parameters=[Parameter.from_dict(item) for item in parameters],
|
||||||
icon=Icon.from_dict(data.get("icon")),
|
icon=Icon.from_dict(data.get("icon")),
|
||||||
properties=dict(data.get("properties", {})),
|
properties=dict(data.get("properties", {})),
|
||||||
@@ -532,11 +533,10 @@ class GraphDocument:
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Component names inside {owner.name!r} must be unique"
|
f"Component names inside {owner.name!r} must be unique"
|
||||||
)
|
)
|
||||||
input_ids = {port.id for port in owner.inputs}
|
port_ids = {port.id for port in owner.ports}
|
||||||
output_ids = {port.id for port in owner.outputs}
|
if len(port_ids) != len(owner.ports):
|
||||||
if len(input_ids) != len(owner.inputs) or len(output_ids) != len(owner.outputs):
|
|
||||||
raise ValueError(f"Component {owner.name} contains duplicate port IDs")
|
raise ValueError(f"Component {owner.name} contains duplicate port IDs")
|
||||||
for port in (*owner.inputs, *owner.outputs):
|
for port in owner.ports:
|
||||||
PortTypeRegistry.get(port.type)
|
PortTypeRegistry.get(port.type)
|
||||||
if port.orientation not in {"input", "output", "indifferent"}:
|
if port.orientation not in {"input", "output", "indifferent"}:
|
||||||
raise ValueError(f"Port {port.name!r} has an invalid orientation")
|
raise ValueError(f"Port {port.name!r} has an invalid orientation")
|
||||||
@@ -553,8 +553,18 @@ class GraphDocument:
|
|||||||
raise ValueError(f"Port {port.name!r} has invalid dimensions")
|
raise ValueError(f"Port {port.name!r} has invalid dimensions")
|
||||||
for junction in owner.graph.junctions.values():
|
for junction in owner.graph.junctions.values():
|
||||||
PortTypeRegistry.get(junction.type)
|
PortTypeRegistry.get(junction.type)
|
||||||
|
if junction.type == "power":
|
||||||
|
raise ValueError("Power bond connections cannot contain junctions")
|
||||||
endpoint_counts: dict[tuple[str, str, str], int] = {}
|
endpoint_counts: dict[tuple[str, str, str], int] = {}
|
||||||
for connection in owner.graph.connections.values():
|
for connection in owner.graph.connections.values():
|
||||||
|
if connection.causality not in {
|
||||||
|
"none",
|
||||||
|
"source",
|
||||||
|
"target",
|
||||||
|
"warn_source",
|
||||||
|
"warn_target",
|
||||||
|
}:
|
||||||
|
raise ValueError(f"Connection {connection.id} has invalid causality")
|
||||||
if connection.source.junction is not None:
|
if connection.source.junction is not None:
|
||||||
junction = owner.graph.junctions.get(connection.source.junction)
|
junction = owner.graph.junctions.get(connection.source.junction)
|
||||||
if junction is None:
|
if junction is None:
|
||||||
@@ -563,17 +573,25 @@ class GraphDocument:
|
|||||||
)
|
)
|
||||||
source_port = Port(junction.id, "Junction", type=junction.type)
|
source_port = Port(junction.id, "Junction", type=junction.type)
|
||||||
elif connection.source.interface is not None:
|
elif connection.source.interface is not None:
|
||||||
if connection.source.interface not in input_ids:
|
source_ports = [
|
||||||
|
port
|
||||||
|
for port in owner.ports
|
||||||
|
if port.orientation in {"input", "indifferent"}
|
||||||
|
]
|
||||||
|
if connection.source.interface not in {port.id for port in source_ports}:
|
||||||
raise ValueError(f"Connection {connection.id} uses an unknown interface input")
|
raise ValueError(f"Connection {connection.id} uses an unknown interface input")
|
||||||
source_port = next(p for p in owner.inputs if p.id == connection.source.interface)
|
source_port = next(
|
||||||
|
port for port in source_ports if port.id == connection.source.interface
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
source = owner.graph.blocks.get(connection.source.block or "")
|
source = owner.graph.blocks.get(connection.source.block or "")
|
||||||
source_ports = (
|
source_ports = (
|
||||||
[]
|
[]
|
||||||
if source is None
|
if source is None
|
||||||
else [
|
else [
|
||||||
*source.outputs,
|
port
|
||||||
*(p for p in source.inputs if p.orientation == "indifferent"),
|
for port in source.ports
|
||||||
|
if port.orientation in {"output", "indifferent"}
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
if source is None or connection.source.port not in {p.id for p in source_ports}:
|
if source is None or connection.source.port not in {p.id for p in source_ports}:
|
||||||
@@ -592,21 +610,34 @@ class GraphDocument:
|
|||||||
allows_multiple_connections=False,
|
allows_multiple_connections=False,
|
||||||
)
|
)
|
||||||
elif connection.target.interface is not None:
|
elif connection.target.interface is not None:
|
||||||
indifferent_ids = {
|
target_ports = [
|
||||||
port.id for port in owner.inputs if port.orientation == "indifferent"
|
port
|
||||||
}
|
for port in owner.ports
|
||||||
if connection.target.interface not in output_ids | indifferent_ids:
|
if port.orientation in {"output", "indifferent"}
|
||||||
|
]
|
||||||
|
if connection.target.interface not in {port.id for port in target_ports}:
|
||||||
raise ValueError(f"Connection {connection.id} uses an unknown interface output")
|
raise ValueError(f"Connection {connection.id} uses an unknown interface output")
|
||||||
target_port = next(
|
target_port = next(
|
||||||
p
|
port for port in target_ports if port.id == connection.target.interface
|
||||||
for p in (*owner.outputs, *owner.inputs)
|
|
||||||
if p.id == connection.target.interface
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
target = owner.graph.blocks.get(connection.target.block or "")
|
target = owner.graph.blocks.get(connection.target.block or "")
|
||||||
if target is None or connection.target.port not in {p.id for p in target.inputs}:
|
target_ports = (
|
||||||
|
[]
|
||||||
|
if target is None
|
||||||
|
else [
|
||||||
|
port
|
||||||
|
for port in target.ports
|
||||||
|
if port.orientation in {"input", "indifferent"}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if target is None or connection.target.port not in {
|
||||||
|
port.id for port in target_ports
|
||||||
|
}:
|
||||||
raise ValueError(f"Connection {connection.id} uses an unknown block input")
|
raise ValueError(f"Connection {connection.id} uses an unknown block input")
|
||||||
target_port = next(p for p in target.inputs if p.id == connection.target.port)
|
target_port = next(
|
||||||
|
port for port in target_ports if port.id == connection.target.port
|
||||||
|
)
|
||||||
if not PortTypeRegistry.compatible(
|
if not PortTypeRegistry.compatible(
|
||||||
source_port.type,
|
source_port.type,
|
||||||
target_port.type,
|
target_port.type,
|
||||||
@@ -614,6 +645,9 @@ class GraphDocument:
|
|||||||
target_port.domain,
|
target_port.domain,
|
||||||
):
|
):
|
||||||
raise ValueError(f"Connection {connection.id} joins incompatible port types")
|
raise ValueError(f"Connection {connection.id} joins incompatible port types")
|
||||||
|
# The endpoint ports remain authoritative. This also upgrades older
|
||||||
|
# documents whose connections predate the explicit type field.
|
||||||
|
connection.type = source_port.type
|
||||||
source_key = (
|
source_key = (
|
||||||
"source-junction"
|
"source-junction"
|
||||||
if connection.source.junction is not None
|
if connection.source.junction is not None
|
||||||
@@ -676,6 +710,8 @@ def clone_component(source: Component) -> Component:
|
|||||||
remap(connection.target),
|
remap(connection.target),
|
||||||
connection.name,
|
connection.name,
|
||||||
deepcopy(connection.properties),
|
deepcopy(connection.properties),
|
||||||
|
connection.type,
|
||||||
|
connection.causality,
|
||||||
)
|
)
|
||||||
for connection in current.graph.connections.values()
|
for connection in current.graph.connections.values()
|
||||||
for new_id in [str(uuid4())]
|
for new_id in [str(uuid4())]
|
||||||
@@ -708,14 +744,7 @@ def clone_component(source: Component) -> Component:
|
|||||||
name=current.name,
|
name=current.name,
|
||||||
x=current.x,
|
x=current.x,
|
||||||
y=current.y,
|
y=current.y,
|
||||||
inputs=[
|
ports=[Port.from_dict(port.to_dict()) for port in current.ports],
|
||||||
Port.from_dict(port.to_dict(), "input")
|
|
||||||
for port in current.inputs
|
|
||||||
],
|
|
||||||
outputs=[
|
|
||||||
Port.from_dict(port.to_dict(), "output")
|
|
||||||
for port in current.outputs
|
|
||||||
],
|
|
||||||
parameters=deepcopy(current.parameters),
|
parameters=deepcopy(current.parameters),
|
||||||
icon=Icon.from_dict(current.icon.to_dict()),
|
icon=Icon.from_dict(current.icon.to_dict()),
|
||||||
properties=deepcopy(current.properties),
|
properties=deepcopy(current.properties),
|
||||||
|
|||||||
@@ -59,9 +59,7 @@ def build_id_list(graph: dict[str, Any]) -> dict[str, Any]:
|
|||||||
def visit(component: dict[str, Any]) -> None:
|
def visit(component: dict[str, Any]) -> None:
|
||||||
add(component, "component")
|
add(component, "component")
|
||||||
interface = component.get("interface", {})
|
interface = component.get("interface", {})
|
||||||
for port in interface.get("inputs", []):
|
for port in interface.get("ports", []):
|
||||||
add(port, "port")
|
|
||||||
for port in interface.get("outputs", []):
|
|
||||||
add(port, "port")
|
add(port, "port")
|
||||||
|
|
||||||
implementation = component.get("implementation", {})
|
implementation = component.get("implementation", {})
|
||||||
@@ -110,10 +108,9 @@ def emit_model(
|
|||||||
)
|
)
|
||||||
|
|
||||||
interface = graph.get("interface", {})
|
interface = graph.get("interface", {})
|
||||||
for port in interface.get("inputs", []):
|
for port in interface.get("ports", []):
|
||||||
lines.append(_port_declaration(port, "input", indent + 1, macros))
|
direction = "output" if port.get("orientation") == "output" else "input"
|
||||||
for port in interface.get("outputs", []):
|
lines.append(_port_declaration(port, direction, indent + 1, macros))
|
||||||
lines.append(_port_declaration(port, "output", indent + 1, macros))
|
|
||||||
|
|
||||||
for parameter in graph.get("parameters", []):
|
for parameter in graph.get("parameters", []):
|
||||||
parameter_type = modelica_type(parameter.get("type", "real"))
|
parameter_type = modelica_type(parameter.get("type", "real"))
|
||||||
@@ -332,7 +329,7 @@ def _port_count_macros(
|
|||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
macros: dict[str, str] = {}
|
macros: dict[str, str] = {}
|
||||||
interface = component.get("interface", {})
|
interface = component.get("interface", {})
|
||||||
for port in (*interface.get("inputs", []), *interface.get("outputs", [])):
|
for port in interface.get("ports", []):
|
||||||
if port.get("multipleConnections", False):
|
if port.get("multipleConnections", False):
|
||||||
macros[f"{identifier(port['name'])}_N"] = str(
|
macros[f"{identifier(port['name'])}_N"] = str(
|
||||||
connection_counts.get(port["id"], 0)
|
connection_counts.get(port["id"], 0)
|
||||||
@@ -354,8 +351,7 @@ def expand_bevalues(text: str, values: dict[str, str]) -> str:
|
|||||||
|
|
||||||
def _find_port(component: dict[str, Any], port_id: str) -> dict[str, Any]:
|
def _find_port(component: dict[str, Any], port_id: str) -> dict[str, Any]:
|
||||||
interface = component.get("interface", {})
|
interface = component.get("interface", {})
|
||||||
ports = [*interface.get("inputs", []), *interface.get("outputs", [])]
|
for port in interface.get("ports", []):
|
||||||
for port in ports:
|
|
||||||
if port.get("id") == port_id:
|
if port.get("id") == port_id:
|
||||||
return port
|
return port
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from collections.abc import Callable
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from bedit.core.application_log import get_logger
|
from bedit.core.application_log import get_logger
|
||||||
|
from bedit.core.bond_graph import infer_causality
|
||||||
from bedit.core.simulation.composer import compose_graph
|
from bedit.core.simulation.composer import compose_graph
|
||||||
from bedit.core.simulation.openmodelica import (
|
from bedit.core.simulation.openmodelica import (
|
||||||
ErrorCallback,
|
ErrorCallback,
|
||||||
@@ -87,6 +88,7 @@ class Simulation:
|
|||||||
def compose_source(self, graph: dict[str, Any]) -> tuple[str, str]:
|
def compose_source(self, graph: dict[str, Any]) -> tuple[str, str]:
|
||||||
"""Compose Modelica source without asking OpenModelica to build it."""
|
"""Compose Modelica source without asking OpenModelica to build it."""
|
||||||
|
|
||||||
|
infer_causality(graph)
|
||||||
result = compose_graph(graph)
|
result = compose_graph(graph)
|
||||||
self.last_composition_input = result.graph
|
self.last_composition_input = result.graph
|
||||||
self.id_list = result.objects_by_id
|
self.id_list = result.objects_by_id
|
||||||
|
|||||||
Binary file not shown.
@@ -1,4 +1,6 @@
|
|||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QCoreApplication, Qt
|
from PySide6.QtCore import QCoreApplication, Qt
|
||||||
from PySide6.QtGui import QColor, QPalette
|
from PySide6.QtGui import QColor, QPalette
|
||||||
@@ -38,14 +40,26 @@ def apply_light_theme(app: QApplication) -> None:
|
|||||||
app.setPalette(palette)
|
app.setPalette(palette)
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def _document_argument(arguments: Sequence[str]) -> Path | None:
|
||||||
|
"""Return the first positional application argument, if present."""
|
||||||
|
|
||||||
|
return next(
|
||||||
|
(Path(argument) for argument in arguments[1:] if not argument.startswith("-")),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
QCoreApplication.setApplicationName("BEdit")
|
QCoreApplication.setApplicationName("BEdit")
|
||||||
QCoreApplication.setOrganizationName("BEdit")
|
QCoreApplication.setOrganizationName("BEdit")
|
||||||
QCoreApplication.setApplicationVersion("0.1.0")
|
QCoreApplication.setApplicationVersion("0.1.0")
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(list(sys.argv if argv is None else argv))
|
||||||
app.setApplicationDisplayName("BEdit")
|
app.setApplicationDisplayName("BEdit")
|
||||||
|
|
||||||
apply_light_theme(app)
|
apply_light_theme(app)
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
|
document_path = _document_argument(app.arguments())
|
||||||
|
if document_path is not None:
|
||||||
|
window.open_document_path(document_path, check_unsaved=False)
|
||||||
window.show()
|
window.show()
|
||||||
return app.exec()
|
return app.exec()
|
||||||
|
|||||||
@@ -311,24 +311,21 @@ class DeleteSelectionCommand(QUndoCommand):
|
|||||||
owner_id: str | None,
|
owner_id: str | None,
|
||||||
blocks: dict[str, Component],
|
blocks: dict[str, Component],
|
||||||
connections: dict[str, Connection],
|
connections: dict[str, Connection],
|
||||||
inputs: list[Port],
|
ports: list[Port],
|
||||||
outputs: list[Port],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__("Delete selection")
|
super().__init__("Delete selection")
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.owner_id = owner_id
|
self.owner_id = owner_id
|
||||||
self.blocks = blocks
|
self.blocks = blocks
|
||||||
self.connections = connections
|
self.connections = connections
|
||||||
self.inputs = inputs
|
self.ports = ports
|
||||||
self.outputs = outputs
|
|
||||||
|
|
||||||
def redo(self) -> None:
|
def redo(self) -> None:
|
||||||
self.controller._delete_items(
|
self.controller._delete_items(
|
||||||
self.owner_id,
|
self.owner_id,
|
||||||
set(self.blocks),
|
set(self.blocks),
|
||||||
set(self.connections),
|
set(self.connections),
|
||||||
{port.id for port in self.inputs},
|
{port.id for port in self.ports},
|
||||||
{port.id for port in self.outputs},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def undo(self) -> None:
|
def undo(self) -> None:
|
||||||
@@ -336,8 +333,7 @@ class DeleteSelectionCommand(QUndoCommand):
|
|||||||
self.owner_id,
|
self.owner_id,
|
||||||
self.blocks,
|
self.blocks,
|
||||||
self.connections,
|
self.connections,
|
||||||
self.inputs,
|
self.ports,
|
||||||
self.outputs,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -361,7 +357,6 @@ class PasteSelectionCommand(QUndoCommand):
|
|||||||
self.blocks,
|
self.blocks,
|
||||||
self.connections,
|
self.connections,
|
||||||
[],
|
[],
|
||||||
[],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def undo(self) -> None:
|
def undo(self) -> None:
|
||||||
@@ -370,7 +365,6 @@ class PasteSelectionCommand(QUndoCommand):
|
|||||||
set(self.blocks),
|
set(self.blocks),
|
||||||
set(self.connections),
|
set(self.connections),
|
||||||
set(),
|
set(),
|
||||||
set(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from bedit.core.model import (
|
|||||||
Port,
|
Port,
|
||||||
clone_component,
|
clone_component,
|
||||||
)
|
)
|
||||||
|
from bedit.core.bond_graph import infer_causality
|
||||||
from bedit.core.simulation import Simulation
|
from bedit.core.simulation import Simulation
|
||||||
from bedit.core.port_types import PortTypeRegistry
|
from bedit.core.port_types import PortTypeRegistry
|
||||||
from bedit.core.serializer import DocumentSerializer
|
from bedit.core.serializer import DocumentSerializer
|
||||||
@@ -307,6 +308,7 @@ class DocumentController(QObject):
|
|||||||
properties={
|
properties={
|
||||||
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
|
"waypoints": [{"x": point.x(), "y": point.y()} for point in (waypoints or [])],
|
||||||
},
|
},
|
||||||
|
type=source_port.type,
|
||||||
)
|
)
|
||||||
self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
|
self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
|
||||||
return connection.id
|
return connection.id
|
||||||
@@ -324,6 +326,8 @@ class DocumentController(QObject):
|
|||||||
if original is None:
|
if original is None:
|
||||||
raise ValueError("The connection no longer exists")
|
raise ValueError("The connection no longer exists")
|
||||||
port_type = self.connection_port_type(original)
|
port_type = self.connection_port_type(original)
|
||||||
|
if port_type == "power":
|
||||||
|
raise ValueError("Power bond connections cannot contain junctions")
|
||||||
junction = Junction(str(uuid4()), position.x(), position.y(), port_type)
|
junction = Junction(str(uuid4()), position.x(), position.y(), port_type)
|
||||||
first_properties = deepcopy(original.properties)
|
first_properties = deepcopy(original.properties)
|
||||||
first_properties["waypoints"] = [
|
first_properties["waypoints"] = [
|
||||||
@@ -338,6 +342,8 @@ class DocumentController(QObject):
|
|||||||
Endpoint(junction=junction.id),
|
Endpoint(junction=junction.id),
|
||||||
original.name,
|
original.name,
|
||||||
first_properties,
|
first_properties,
|
||||||
|
original.type,
|
||||||
|
original.causality,
|
||||||
)
|
)
|
||||||
second = Connection(
|
second = Connection(
|
||||||
str(uuid4()),
|
str(uuid4()),
|
||||||
@@ -345,6 +351,8 @@ class DocumentController(QObject):
|
|||||||
original.target,
|
original.target,
|
||||||
"",
|
"",
|
||||||
second_properties,
|
second_properties,
|
||||||
|
original.type,
|
||||||
|
original.causality,
|
||||||
)
|
)
|
||||||
self.undo_stack.push(
|
self.undo_stack.push(
|
||||||
SplitConnectionCommand(
|
SplitConnectionCommand(
|
||||||
@@ -442,12 +450,14 @@ class DocumentController(QObject):
|
|||||||
component = self.active_component
|
component = self.active_component
|
||||||
if component is None or component.implementation_kind != "graph":
|
if component is None or component.implementation_kind != "graph":
|
||||||
raise ValueError("Open a graph component before composing")
|
raise ValueError("Open a graph component before composing")
|
||||||
|
self._infer_active_graph_causality(component)
|
||||||
self.simulation.compose(component.to_dict())
|
self.simulation.compose(component.to_dict())
|
||||||
|
|
||||||
def compose_active_graph_source(self) -> tuple[str, str]:
|
def compose_active_graph_source(self) -> tuple[str, str]:
|
||||||
component = self.active_component
|
component = self.active_component
|
||||||
if component is None or component.implementation_kind != "graph":
|
if component is None or component.implementation_kind != "graph":
|
||||||
raise ValueError("Open a graph component before exporting a model")
|
raise ValueError("Open a graph component before exporting a model")
|
||||||
|
self._infer_active_graph_causality(component)
|
||||||
return self.simulation.compose_source(component.to_dict())
|
return self.simulation.compose_source(component.to_dict())
|
||||||
|
|
||||||
def run_simulation(
|
def run_simulation(
|
||||||
@@ -460,6 +470,7 @@ class DocumentController(QObject):
|
|||||||
component = self.active_component
|
component = self.active_component
|
||||||
if component is None or component.implementation_kind != "graph":
|
if component is None or component.implementation_kind != "graph":
|
||||||
raise ValueError("Open a graph component before running a simulation")
|
raise ValueError("Open a graph component before running a simulation")
|
||||||
|
self._infer_active_graph_causality(component)
|
||||||
self.simulation.run_simulation(
|
self.simulation.run_simulation(
|
||||||
component.to_dict(),
|
component.to_dict(),
|
||||||
progress_callback,
|
progress_callback,
|
||||||
@@ -468,6 +479,34 @@ class DocumentController(QObject):
|
|||||||
error_callback,
|
error_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _infer_active_graph_causality(self, component: Component) -> None:
|
||||||
|
"""Infer causality and copy the derived values into the live model."""
|
||||||
|
|
||||||
|
inferred = infer_causality(component.to_dict())
|
||||||
|
causalities: dict[str, str] = {}
|
||||||
|
|
||||||
|
def collect(serialized_component: dict) -> None:
|
||||||
|
graph = serialized_component.get("implementation", {}).get("graph", {})
|
||||||
|
for connection in graph.get("connections", []):
|
||||||
|
causalities[str(connection["id"])] = str(
|
||||||
|
connection.get("causality", "none")
|
||||||
|
)
|
||||||
|
for block in graph.get("blocks", []):
|
||||||
|
collect(block)
|
||||||
|
|
||||||
|
collect(inferred)
|
||||||
|
changed = False
|
||||||
|
for nested_component in self._component_subtree(component):
|
||||||
|
for connection in nested_component.graph.connections.values():
|
||||||
|
causality = causalities.get(connection.id, "none")
|
||||||
|
if connection.causality != causality:
|
||||||
|
connection.causality = causality
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
if self.document is not None:
|
||||||
|
self.document.validate()
|
||||||
|
self.documentReset.emit()
|
||||||
|
|
||||||
def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None:
|
def set_route_waypoints(self, item_kind: str, item_id: str, waypoints: list[QPointF]) -> None:
|
||||||
item = (
|
item = (
|
||||||
self.active_graph.connections
|
self.active_graph.connections
|
||||||
@@ -577,24 +616,25 @@ class DocumentController(QObject):
|
|||||||
allows_multiple_connections=role == "source",
|
allows_multiple_connections=role == "source",
|
||||||
)
|
)
|
||||||
if endpoint.interface is not None:
|
if endpoint.interface is not None:
|
||||||
ports = owner.inputs if role == "source" else owner.outputs
|
orientations = (
|
||||||
|
{"input", "indifferent"}
|
||||||
|
if role == "source"
|
||||||
|
else {"output", "indifferent"}
|
||||||
|
)
|
||||||
ports = [
|
ports = [
|
||||||
*ports,
|
port for port in owner.ports if port.orientation in orientations
|
||||||
*(
|
|
||||||
port
|
|
||||||
for port in owner.inputs
|
|
||||||
if port.orientation == "indifferent" and port not in ports
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
component = owner.graph.blocks.get(endpoint.block or "")
|
component = owner.graph.blocks.get(endpoint.block or "")
|
||||||
if component is None:
|
if component is None:
|
||||||
return None
|
return None
|
||||||
ports = component.outputs if role == "source" else component.inputs
|
orientations = (
|
||||||
if role == "source":
|
{"output", "indifferent"}
|
||||||
|
if role == "source"
|
||||||
|
else {"input", "indifferent"}
|
||||||
|
)
|
||||||
ports = [
|
ports = [
|
||||||
*ports,
|
port for port in component.ports if port.orientation in orientations
|
||||||
*(port for port in component.inputs if port.orientation == "indifferent"),
|
|
||||||
]
|
]
|
||||||
return next(
|
return next(
|
||||||
(port for port in ports if port.id == (endpoint.interface or endpoint.port)), None
|
(port for port in ports if port.id == (endpoint.interface or endpoint.port)), None
|
||||||
@@ -619,12 +659,13 @@ class DocumentController(QObject):
|
|||||||
component = self.active_component
|
component = self.active_component
|
||||||
if component is None or component.implementation_kind != "graph":
|
if component is None or component.implementation_kind != "graph":
|
||||||
raise ValueError("Open a graph component before adding an interface")
|
raise ValueError("Open a graph component before adding an interface")
|
||||||
ports = component.inputs if direction == "input" else component.outputs
|
ports = [port for port in component.ports if port.orientation == direction]
|
||||||
port = Port(
|
port = Port(
|
||||||
id=f"{direction}-{uuid4().hex[:8]}",
|
id=f"{direction}-{uuid4().hex[:8]}",
|
||||||
name=f"{direction.title()} {len(ports) + 1}",
|
name=f"{direction.title()} {len(ports) + 1}",
|
||||||
x=position.x(),
|
x=position.x(),
|
||||||
y=position.y(),
|
y=position.y(),
|
||||||
|
orientation=direction,
|
||||||
)
|
)
|
||||||
self.undo_stack.push(AddInterfacePortCommand(self, component.id, direction, port))
|
self.undo_stack.push(AddInterfacePortCommand(self, component.id, direction, port))
|
||||||
return port.id
|
return port.id
|
||||||
@@ -639,7 +680,7 @@ class DocumentController(QObject):
|
|||||||
owner = self.active_component
|
owner = self.active_component
|
||||||
if owner is None:
|
if owner is None:
|
||||||
return
|
return
|
||||||
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
port = next((port for port in owner.ports if port.id == port_id), None)
|
||||||
if port is not None and port.name != name:
|
if port is not None and port.name != name:
|
||||||
self.undo_stack.push(
|
self.undo_stack.push(
|
||||||
RenameInterfacePortCommand(self, owner.id, port_id, port.name, name)
|
RenameInterfacePortCommand(self, owner.id, port_id, port.name, name)
|
||||||
@@ -670,8 +711,7 @@ class DocumentController(QObject):
|
|||||||
|
|
||||||
def replace_active_text_definition(
|
def replace_active_text_definition(
|
||||||
self,
|
self,
|
||||||
inputs: list[Port],
|
ports: list[Port],
|
||||||
outputs: list[Port],
|
|
||||||
declarations: str,
|
declarations: str,
|
||||||
initial_equations: str,
|
initial_equations: str,
|
||||||
equations: str,
|
equations: str,
|
||||||
@@ -680,14 +720,17 @@ class DocumentController(QObject):
|
|||||||
component = self.active_component
|
component = self.active_component
|
||||||
if component is None or component.implementation_kind != "text":
|
if component is None or component.implementation_kind != "text":
|
||||||
raise ValueError("Only text-defined components can be edited here")
|
raise ValueError("Only text-defined components can be edited here")
|
||||||
input_ids = [port.id for port in inputs]
|
port_ids = [port.id for port in ports]
|
||||||
output_ids = [port.id for port in outputs]
|
input_ids = {
|
||||||
source_ids = output_ids + [
|
port.id for port in ports if port.orientation in {"input", "indifferent"}
|
||||||
port.id for port in inputs if port.orientation == "indifferent"
|
}
|
||||||
]
|
output_ids = {
|
||||||
if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids):
|
port.id for port in ports if port.orientation in {"output", "indifferent"}
|
||||||
raise ValueError("Input and output IDs must be unique")
|
}
|
||||||
if any(not port.name.strip() for port in (*inputs, *outputs)):
|
source_ids = output_ids
|
||||||
|
if len(set(port_ids)) != len(port_ids):
|
||||||
|
raise ValueError("Port IDs must be unique")
|
||||||
|
if any(not port.name.strip() for port in ports):
|
||||||
raise ValueError("Every port must have a name")
|
raise ValueError("Every port must have a name")
|
||||||
parameter_ids = [parameter.id for parameter in parameters]
|
parameter_ids = [parameter.id for parameter in parameters]
|
||||||
if len(set(parameter_ids)) != len(parameter_ids):
|
if len(set(parameter_ids)) != len(parameter_ids):
|
||||||
@@ -713,14 +756,12 @@ class DocumentController(QObject):
|
|||||||
f"Output {connection.source.port!r} is still connected in the containing graph"
|
f"Output {connection.source.port!r} is still connected in the containing graph"
|
||||||
)
|
)
|
||||||
old = {
|
old = {
|
||||||
"inputs": [port.to_dict() for port in component.inputs],
|
"ports": [port.to_dict() for port in component.ports],
|
||||||
"outputs": [port.to_dict() for port in component.outputs],
|
|
||||||
"source": deepcopy(component.source),
|
"source": deepcopy(component.source),
|
||||||
"parameters": [parameter.to_dict() for parameter in component.parameters],
|
"parameters": [parameter.to_dict() for parameter in component.parameters],
|
||||||
}
|
}
|
||||||
new = {
|
new = {
|
||||||
"inputs": [port.to_dict() for port in inputs],
|
"ports": [port.to_dict() for port in ports],
|
||||||
"outputs": [port.to_dict() for port in outputs],
|
|
||||||
"source": {
|
"source": {
|
||||||
"equations": equations,
|
"equations": equations,
|
||||||
"declarations": declarations,
|
"declarations": declarations,
|
||||||
@@ -731,8 +772,7 @@ class DocumentController(QObject):
|
|||||||
if old != new:
|
if old != new:
|
||||||
candidate = deepcopy(self.document)
|
candidate = deepcopy(self.document)
|
||||||
candidate_component = candidate.find_component(component.id)
|
candidate_component = candidate.find_component(component.id)
|
||||||
candidate_component.inputs = deepcopy(inputs)
|
candidate_component.ports = deepcopy(ports)
|
||||||
candidate_component.outputs = deepcopy(outputs)
|
|
||||||
candidate_component.source = deepcopy(new["source"])
|
candidate_component.source = deepcopy(new["source"])
|
||||||
candidate_component.parameters = deepcopy(parameters)
|
candidate_component.parameters = deepcopy(parameters)
|
||||||
candidate.validate()
|
candidate.validate()
|
||||||
@@ -743,8 +783,7 @@ class DocumentController(QObject):
|
|||||||
component_id: str,
|
component_id: str,
|
||||||
name: str,
|
name: str,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
inputs: list[Port],
|
ports: list[Port],
|
||||||
outputs: list[Port],
|
|
||||||
show_subtree: bool,
|
show_subtree: bool,
|
||||||
show_name: bool,
|
show_name: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -759,8 +798,7 @@ class DocumentController(QObject):
|
|||||||
old = {
|
old = {
|
||||||
"name": component.name,
|
"name": component.name,
|
||||||
"icon": component.icon.to_dict(),
|
"icon": component.icon.to_dict(),
|
||||||
"inputs": [port.to_dict() for port in component.inputs],
|
"ports": [port.to_dict() for port in component.ports],
|
||||||
"outputs": [port.to_dict() for port in component.outputs],
|
|
||||||
"show_subtree": component.show_subtree_in_library,
|
"show_subtree": component.show_subtree_in_library,
|
||||||
"properties": deepcopy(component.properties),
|
"properties": deepcopy(component.properties),
|
||||||
}
|
}
|
||||||
@@ -772,8 +810,7 @@ class DocumentController(QObject):
|
|||||||
new = {
|
new = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"icon": icon.to_dict(),
|
"icon": icon.to_dict(),
|
||||||
"inputs": [port.to_dict() for port in inputs],
|
"ports": [port.to_dict() for port in ports],
|
||||||
"outputs": [port.to_dict() for port in outputs],
|
|
||||||
"show_subtree": show_subtree,
|
"show_subtree": show_subtree,
|
||||||
"properties": properties,
|
"properties": properties,
|
||||||
}
|
}
|
||||||
@@ -782,8 +819,7 @@ class DocumentController(QObject):
|
|||||||
candidate_component = candidate.find_component(component_id)
|
candidate_component = candidate.find_component(component_id)
|
||||||
candidate_component.name = name
|
candidate_component.name = name
|
||||||
candidate_component.icon = Icon.from_dict(icon.to_dict())
|
candidate_component.icon = Icon.from_dict(icon.to_dict())
|
||||||
candidate_component.inputs = deepcopy(inputs)
|
candidate_component.ports = deepcopy(ports)
|
||||||
candidate_component.outputs = deepcopy(outputs)
|
|
||||||
candidate_component.properties = deepcopy(properties)
|
candidate_component.properties = deepcopy(properties)
|
||||||
candidate.validate()
|
candidate.validate()
|
||||||
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
||||||
@@ -853,19 +889,19 @@ class DocumentController(QObject):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def edit_component_ports(
|
def edit_component_ports(self, component_id: str, ports: list[Port]) -> None:
|
||||||
self, component_id: str, inputs: list[Port], outputs: list[Port]
|
|
||||||
) -> None:
|
|
||||||
if self.document is None:
|
if self.document is None:
|
||||||
return
|
return
|
||||||
component = self.document.find_component(component_id)
|
component = self.document.find_component(component_id)
|
||||||
if component is None:
|
if component is None:
|
||||||
return
|
return
|
||||||
input_ids = {port.id for port in inputs}
|
input_ids = {
|
||||||
output_ids = {port.id for port in outputs}
|
port.id for port in ports if port.orientation in {"input", "indifferent"}
|
||||||
source_ids = output_ids | {
|
|
||||||
port.id for port in inputs if port.orientation == "indifferent"
|
|
||||||
}
|
}
|
||||||
|
output_ids = {
|
||||||
|
port.id for port in ports if port.orientation in {"output", "indifferent"}
|
||||||
|
}
|
||||||
|
source_ids = output_ids
|
||||||
parent = self.document.find_parent(component_id)
|
parent = self.document.find_parent(component_id)
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
for connection in parent.graph.connections.values():
|
for connection in parent.graph.connections.values():
|
||||||
@@ -886,21 +922,18 @@ class DocumentController(QObject):
|
|||||||
raise ValueError("An interface output cannot be removed while connected")
|
raise ValueError("An interface output cannot be removed while connected")
|
||||||
candidate = deepcopy(self.document)
|
candidate = deepcopy(self.document)
|
||||||
candidate_component = candidate.find_component(component_id)
|
candidate_component = candidate.find_component(component_id)
|
||||||
candidate_component.inputs = deepcopy(inputs)
|
candidate_component.ports = deepcopy(ports)
|
||||||
candidate_component.outputs = deepcopy(outputs)
|
|
||||||
candidate.validate()
|
candidate.validate()
|
||||||
old = {
|
old = {
|
||||||
"name": component.name,
|
"name": component.name,
|
||||||
"icon": component.icon.to_dict(),
|
"icon": component.icon.to_dict(),
|
||||||
"inputs": [port.to_dict() for port in component.inputs],
|
"ports": [port.to_dict() for port in component.ports],
|
||||||
"outputs": [port.to_dict() for port in component.outputs],
|
|
||||||
"show_subtree": component.show_subtree_in_library,
|
"show_subtree": component.show_subtree_in_library,
|
||||||
"properties": deepcopy(component.properties),
|
"properties": deepcopy(component.properties),
|
||||||
}
|
}
|
||||||
new = {
|
new = {
|
||||||
**old,
|
**old,
|
||||||
"inputs": [port.to_dict() for port in inputs],
|
"ports": [port.to_dict() for port in ports],
|
||||||
"outputs": [port.to_dict() for port in outputs],
|
|
||||||
}
|
}
|
||||||
if old != new:
|
if old != new:
|
||||||
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
||||||
@@ -930,8 +963,7 @@ class DocumentController(QObject):
|
|||||||
self,
|
self,
|
||||||
block_ids: set[str],
|
block_ids: set[str],
|
||||||
connection_ids: set[str],
|
connection_ids: set[str],
|
||||||
input_ids: set[str],
|
port_ids: set[str],
|
||||||
output_ids: set[str],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
component = self.active_component
|
component = self.active_component
|
||||||
if component is None or component.implementation_kind != "graph":
|
if component is None or component.implementation_kind != "graph":
|
||||||
@@ -942,8 +974,8 @@ class DocumentController(QObject):
|
|||||||
if (
|
if (
|
||||||
connection.source.block in block_ids
|
connection.source.block in block_ids
|
||||||
or connection.target.block in block_ids
|
or connection.target.block in block_ids
|
||||||
or connection.source.interface in input_ids
|
or connection.source.interface in port_ids
|
||||||
or connection.target.interface in output_ids
|
or connection.target.interface in port_ids
|
||||||
):
|
):
|
||||||
all_connection_ids.add(connection.id)
|
all_connection_ids.add(connection.id)
|
||||||
blocks = {
|
blocks = {
|
||||||
@@ -954,9 +986,8 @@ class DocumentController(QObject):
|
|||||||
for connection_id in all_connection_ids
|
for connection_id in all_connection_ids
|
||||||
if connection_id in graph.connections
|
if connection_id in graph.connections
|
||||||
}
|
}
|
||||||
inputs = [port for port in component.inputs if port.id in input_ids]
|
ports = [port for port in component.ports if port.id in port_ids]
|
||||||
outputs = [port for port in component.outputs if port.id in output_ids]
|
if not (blocks or connections or ports):
|
||||||
if not (blocks or connections or inputs or outputs):
|
|
||||||
return
|
return
|
||||||
self.undo_stack.push(
|
self.undo_stack.push(
|
||||||
DeleteSelectionCommand(
|
DeleteSelectionCommand(
|
||||||
@@ -964,8 +995,7 @@ class DocumentController(QObject):
|
|||||||
component.id,
|
component.id,
|
||||||
blocks,
|
blocks,
|
||||||
connections,
|
connections,
|
||||||
inputs,
|
ports,
|
||||||
outputs,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1003,6 +1033,8 @@ class DocumentController(QObject):
|
|||||||
target=Endpoint(block=id_map[source.target.block], port=source.target.port),
|
target=Endpoint(block=id_map[source.target.block], port=source.target.port),
|
||||||
name=source.name,
|
name=source.name,
|
||||||
properties=properties,
|
properties=properties,
|
||||||
|
type=source.type,
|
||||||
|
causality=source.causality,
|
||||||
)
|
)
|
||||||
connections[connection.id] = connection
|
connections[connection.id] = connection
|
||||||
if blocks:
|
if blocks:
|
||||||
@@ -1063,6 +1095,8 @@ class DocumentController(QObject):
|
|||||||
),
|
),
|
||||||
name=source.name,
|
name=source.name,
|
||||||
properties=properties,
|
properties=properties,
|
||||||
|
type=source.type,
|
||||||
|
causality=source.causality,
|
||||||
)
|
)
|
||||||
connections[connection.id] = connection
|
connections[connection.id] = connection
|
||||||
|
|
||||||
@@ -1278,9 +1312,9 @@ class DocumentController(QObject):
|
|||||||
owner = self.document.find_component(owner_id)
|
owner = self.document.find_component(owner_id)
|
||||||
if owner is None:
|
if owner is None:
|
||||||
return
|
return
|
||||||
ports = owner.inputs if direction == "input" else owner.outputs
|
port.orientation = direction
|
||||||
if all(existing.id != port.id for existing in ports):
|
if all(existing.id != port.id for existing in owner.ports):
|
||||||
ports.append(port)
|
owner.ports.append(port)
|
||||||
self.interfaceChanged.emit()
|
self.interfaceChanged.emit()
|
||||||
self.documentReset.emit()
|
self.documentReset.emit()
|
||||||
|
|
||||||
@@ -1290,8 +1324,7 @@ class DocumentController(QObject):
|
|||||||
owner = self.document.find_component(owner_id)
|
owner = self.document.find_component(owner_id)
|
||||||
if owner is None:
|
if owner is None:
|
||||||
return
|
return
|
||||||
ports = owner.inputs if direction == "input" else owner.outputs
|
owner.ports[:] = [port for port in owner.ports if port.id != port_id]
|
||||||
ports[:] = [port for port in ports if port.id != port_id]
|
|
||||||
self.interfaceChanged.emit()
|
self.interfaceChanged.emit()
|
||||||
self.documentReset.emit()
|
self.documentReset.emit()
|
||||||
|
|
||||||
@@ -1301,7 +1334,7 @@ class DocumentController(QObject):
|
|||||||
owner = self.document.find_component(owner_id)
|
owner = self.document.find_component(owner_id)
|
||||||
if owner is None:
|
if owner is None:
|
||||||
return
|
return
|
||||||
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
port = next((port for port in owner.ports if port.id == port_id), None)
|
||||||
if port is not None:
|
if port is not None:
|
||||||
port.x, port.y = position.x(), position.y()
|
port.x, port.y = position.x(), position.y()
|
||||||
self.interfaceChanged.emit()
|
self.interfaceChanged.emit()
|
||||||
@@ -1311,7 +1344,7 @@ class DocumentController(QObject):
|
|||||||
owner = self.document.find_component(owner_id) if self.document else None
|
owner = self.document.find_component(owner_id) if self.document else None
|
||||||
if owner is None:
|
if owner is None:
|
||||||
return
|
return
|
||||||
port = next((p for p in (*owner.inputs, *owner.outputs) if p.id == port_id), None)
|
port = next((port for port in owner.ports if port.id == port_id), None)
|
||||||
if port is not None:
|
if port is not None:
|
||||||
port.name = name
|
port.name = name
|
||||||
self.interfaceChanged.emit()
|
self.interfaceChanged.emit()
|
||||||
@@ -1342,8 +1375,7 @@ class DocumentController(QObject):
|
|||||||
return
|
return
|
||||||
component.name = values["name"]
|
component.name = values["name"]
|
||||||
component.icon = Icon.from_dict(values["icon"])
|
component.icon = Icon.from_dict(values["icon"])
|
||||||
component.inputs = [Port.from_dict(port) for port in values["inputs"]]
|
component.ports = [Port.from_dict(port) for port in values["ports"]]
|
||||||
component.outputs = [Port.from_dict(port) for port in values["outputs"]]
|
|
||||||
component.show_subtree_in_library = values["show_subtree"]
|
component.show_subtree_in_library = values["show_subtree"]
|
||||||
component.properties = deepcopy(values["properties"])
|
component.properties = deepcopy(values["properties"])
|
||||||
self.documentReset.emit()
|
self.documentReset.emit()
|
||||||
@@ -1374,8 +1406,7 @@ class DocumentController(QObject):
|
|||||||
owner_id: str | None,
|
owner_id: str | None,
|
||||||
block_ids: set[str],
|
block_ids: set[str],
|
||||||
connection_ids: set[str],
|
connection_ids: set[str],
|
||||||
input_ids: set[str],
|
port_ids: set[str],
|
||||||
output_ids: set[str],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.document is None:
|
if self.document is None:
|
||||||
return
|
return
|
||||||
@@ -1398,8 +1429,7 @@ class DocumentController(QObject):
|
|||||||
owner.graph.blocks.pop(block_id, None)
|
owner.graph.blocks.pop(block_id, None)
|
||||||
for connection_id in connection_ids:
|
for connection_id in connection_ids:
|
||||||
owner.graph.connections.pop(connection_id, None)
|
owner.graph.connections.pop(connection_id, None)
|
||||||
owner.inputs[:] = [port for port in owner.inputs if port.id not in input_ids]
|
owner.ports[:] = [port for port in owner.ports if port.id not in port_ids]
|
||||||
owner.outputs[:] = [port for port in owner.outputs if port.id not in output_ids]
|
|
||||||
if active_was_deleted:
|
if active_was_deleted:
|
||||||
self.active_component_id = owner_id or next(iter(self.document.roots), None)
|
self.active_component_id = owner_id or next(iter(self.document.roots), None)
|
||||||
self.activeGraphChanged.emit()
|
self.activeGraphChanged.emit()
|
||||||
@@ -1410,8 +1440,7 @@ class DocumentController(QObject):
|
|||||||
owner_id: str | None,
|
owner_id: str | None,
|
||||||
blocks: dict[str, Component],
|
blocks: dict[str, Component],
|
||||||
connections: dict[str, Connection],
|
connections: dict[str, Connection],
|
||||||
inputs: list[Port],
|
ports: list[Port],
|
||||||
outputs: list[Port],
|
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.document is None:
|
if self.document is None:
|
||||||
return
|
return
|
||||||
@@ -1423,10 +1452,8 @@ class DocumentController(QObject):
|
|||||||
return
|
return
|
||||||
owner.graph.blocks.update(blocks)
|
owner.graph.blocks.update(blocks)
|
||||||
owner.graph.connections.update(connections)
|
owner.graph.connections.update(connections)
|
||||||
existing_inputs = {port.id for port in owner.inputs}
|
existing_ports = {port.id for port in owner.ports}
|
||||||
existing_outputs = {port.id for port in owner.outputs}
|
owner.ports.extend(port for port in ports if port.id not in existing_ports)
|
||||||
owner.inputs.extend(port for port in inputs if port.id not in existing_inputs)
|
|
||||||
owner.outputs.extend(port for port in outputs if port.id not in existing_outputs)
|
|
||||||
self.documentReset.emit()
|
self.documentReset.emit()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1441,8 +1468,7 @@ class DocumentController(QObject):
|
|||||||
component = self.document.find_component(component_id)
|
component = self.document.find_component(component_id)
|
||||||
if component is None:
|
if component is None:
|
||||||
return
|
return
|
||||||
component.inputs = [Port.from_dict(item) for item in values["inputs"]]
|
component.ports = [Port.from_dict(item) for item in values["ports"]]
|
||||||
component.outputs = [Port.from_dict(item) for item in values["outputs"]]
|
|
||||||
component.source = deepcopy(values["source"])
|
component.source = deepcopy(values["source"])
|
||||||
component.parameters = [
|
component.parameters = [
|
||||||
Parameter.from_dict(item) for item in values.get("parameters", [])
|
Parameter.from_dict(item) for item in values.get("parameters", [])
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ class ComponentOptionsDialog(QDialog):
|
|||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self.component = component
|
self.component = component
|
||||||
self.edited_icon = component.icon
|
self.edited_icon = component.icon
|
||||||
self.edited_inputs = component.inputs
|
self.edited_ports = component.ports
|
||||||
self.edited_outputs = component.outputs
|
|
||||||
self.ui.nameEdit.setText(component.name)
|
self.ui.nameEdit.setText(component.name)
|
||||||
self.ui.editIconButton.clicked.connect(self.edit_icon)
|
self.ui.editIconButton.clicked.connect(self.edit_icon)
|
||||||
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
|
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
|
||||||
@@ -22,13 +21,11 @@ class ComponentOptionsDialog(QDialog):
|
|||||||
def edit_icon(self) -> None:
|
def edit_icon(self) -> None:
|
||||||
working = Component.from_dict(self.component.to_dict())
|
working = Component.from_dict(self.component.to_dict())
|
||||||
working.icon = self.edited_icon
|
working.icon = self.edited_icon
|
||||||
working.inputs = self.edited_inputs
|
working.ports = self.edited_ports
|
||||||
working.outputs = self.edited_outputs
|
|
||||||
dialog = IconEditorDialog(working, self)
|
dialog = IconEditorDialog(working, self)
|
||||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||||
self.edited_icon = dialog.icon
|
self.edited_icon = dialog.icon
|
||||||
self.edited_inputs = dialog.inputs
|
self.edited_ports = dialog.ports
|
||||||
self.edited_outputs = dialog.outputs
|
|
||||||
|
|
||||||
def accept(self) -> None:
|
def accept(self) -> None:
|
||||||
if not self.ui.nameEdit.text().strip():
|
if not self.ui.nameEdit.text().strip():
|
||||||
|
|||||||
@@ -14,13 +14,6 @@ from bedit.gui.generated.ui_port_options_dialog import Ui_PortOptionsDialog
|
|||||||
PORT_ROLE = Qt.ItemDataRole.UserRole
|
PORT_ROLE = Qt.ItemDataRole.UserRole
|
||||||
|
|
||||||
|
|
||||||
def _oriented_copy(port: Port, fallback: str) -> Port:
|
|
||||||
copied = deepcopy(port)
|
|
||||||
if copied.orientation != "indifferent":
|
|
||||||
copied.orientation = fallback
|
|
||||||
return copied
|
|
||||||
|
|
||||||
|
|
||||||
class PortOptionsDialog(QDialog):
|
class PortOptionsDialog(QDialog):
|
||||||
"""Unified editor for a component's typed, oriented ports."""
|
"""Unified editor for a component's typed, oriented ports."""
|
||||||
|
|
||||||
@@ -32,10 +25,7 @@ class PortOptionsDialog(QDialog):
|
|||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self.setWindowTitle(f"Port Options — {component.name}")
|
self.setWindowTitle(f"Port Options — {component.name}")
|
||||||
self.read_only = read_only
|
self.read_only = read_only
|
||||||
self.ports: list[tuple[Port, str]] = [
|
self.ports = deepcopy(component.ports)
|
||||||
*((_oriented_copy(port, "input"), port.orientation or "input") for port in component.inputs),
|
|
||||||
*((_oriented_copy(port, "output"), "output") for port in component.outputs),
|
|
||||||
]
|
|
||||||
self._loading = False
|
self._loading = False
|
||||||
self.ui.typeCombo.clear()
|
self.ui.typeCombo.clear()
|
||||||
for port_type in PortTypeRegistry.all():
|
for port_type in PortTypeRegistry.all():
|
||||||
@@ -77,22 +67,10 @@ class PortOptionsDialog(QDialog):
|
|||||||
self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).hide()
|
self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).hide()
|
||||||
self._rebuild_list(0 if self.ports else -1)
|
self._rebuild_list(0 if self.ports else -1)
|
||||||
|
|
||||||
@property
|
|
||||||
def inputs(self) -> list[Port]:
|
|
||||||
return [
|
|
||||||
port
|
|
||||||
for port, orientation in self.ports
|
|
||||||
if orientation in {"input", "indifferent"}
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def outputs(self) -> list[Port]:
|
|
||||||
return [port for port, orientation in self.ports if orientation == "output"]
|
|
||||||
|
|
||||||
def _rebuild_list(self, row: int = -1) -> None:
|
def _rebuild_list(self, row: int = -1) -> None:
|
||||||
self.ui.portList.clear()
|
self.ui.portList.clear()
|
||||||
for port, orientation in self.ports:
|
for port in self.ports:
|
||||||
item = QListWidgetItem(f"{port.name} [{orientation}, {port.type}]")
|
item = QListWidgetItem(f"{port.name} [{port.orientation}, {port.type}]")
|
||||||
item.setData(PORT_ROLE, port.id)
|
item.setData(PORT_ROLE, port.id)
|
||||||
self.ui.portList.addItem(item)
|
self.ui.portList.addItem(item)
|
||||||
self.ui.portList.setCurrentRow(min(row, len(self.ports) - 1))
|
self.ui.portList.setCurrentRow(min(row, len(self.ports) - 1))
|
||||||
@@ -102,10 +80,12 @@ class PortOptionsDialog(QDialog):
|
|||||||
self._loading = True
|
self._loading = True
|
||||||
enabled = 0 <= row < len(self.ports)
|
enabled = 0 <= row < len(self.ports)
|
||||||
if enabled:
|
if enabled:
|
||||||
port, orientation = self.ports[row]
|
port = self.ports[row]
|
||||||
self.ui.nameEdit.setText(port.name)
|
self.ui.nameEdit.setText(port.name)
|
||||||
self.ui.typeCombo.setCurrentIndex(self.ui.typeCombo.findData(port.type))
|
self.ui.typeCombo.setCurrentIndex(self.ui.typeCombo.findData(port.type))
|
||||||
self.ui.orientationCombo.setCurrentIndex(self.ui.orientationCombo.findData(orientation))
|
self.ui.orientationCombo.setCurrentIndex(
|
||||||
|
self.ui.orientationCombo.findData(port.orientation)
|
||||||
|
)
|
||||||
self.ui.multipleConnectionsCheckBox.setChecked(
|
self.ui.multipleConnectionsCheckBox.setChecked(
|
||||||
port.allows_multiple_connections
|
port.allows_multiple_connections
|
||||||
)
|
)
|
||||||
@@ -161,7 +141,7 @@ class PortOptionsDialog(QDialog):
|
|||||||
row = self.ui.portList.currentRow()
|
row = self.ui.portList.currentRow()
|
||||||
if self._loading or not (0 <= row < len(self.ports)):
|
if self._loading or not (0 <= row < len(self.ports)):
|
||||||
return
|
return
|
||||||
port, _orientation = self.ports[row]
|
port = self.ports[row]
|
||||||
port.name = self.ui.nameEdit.text()
|
port.name = self.ui.nameEdit.text()
|
||||||
port.type = self.ui.typeCombo.currentData()
|
port.type = self.ui.typeCombo.currentData()
|
||||||
port.allows_multiple_connections = self.ui.multipleConnectionsCheckBox.isChecked()
|
port.allows_multiple_connections = self.ui.multipleConnectionsCheckBox.isChecked()
|
||||||
@@ -179,18 +159,12 @@ class PortOptionsDialog(QDialog):
|
|||||||
)
|
)
|
||||||
orientation = self.ui.orientationCombo.currentData()
|
orientation = self.ui.orientationCombo.currentData()
|
||||||
port.orientation = orientation
|
port.orientation = orientation
|
||||||
self.ports[row] = (port, orientation)
|
self.ui.portList.item(row).setText(f"{port.name} [{port.orientation}, {port.type}]")
|
||||||
self.ui.portList.item(row).setText(
|
|
||||||
f"{port.name} [{self.ports[row][1]}, {port.type}]"
|
|
||||||
)
|
|
||||||
self.edited.emit()
|
self.edited.emit()
|
||||||
|
|
||||||
def set_ports(self, inputs: list[Port], outputs: list[Port]) -> None:
|
def set_ports(self, ports: list[Port]) -> None:
|
||||||
self._loading = True
|
self._loading = True
|
||||||
self.ports = [
|
self.ports = deepcopy(ports)
|
||||||
*((_oriented_copy(port, "input"), port.orientation or "input") for port in inputs),
|
|
||||||
*((_oriented_copy(port, "output"), "output") for port in outputs),
|
|
||||||
]
|
|
||||||
self._loading = False
|
self._loading = False
|
||||||
self._rebuild_list(0 if self.ports else -1)
|
self._rebuild_list(0 if self.ports else -1)
|
||||||
|
|
||||||
@@ -201,7 +175,7 @@ class PortOptionsDialog(QDialog):
|
|||||||
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
|
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
|
||||||
type="signal",
|
type="signal",
|
||||||
)
|
)
|
||||||
self.ports.append((port, "input"))
|
self.ports.append(port)
|
||||||
self._rebuild_list(len(self.ports) - 1)
|
self._rebuild_list(len(self.ports) - 1)
|
||||||
self.ui.nameEdit.selectAll()
|
self.ui.nameEdit.selectAll()
|
||||||
self.ui.nameEdit.setFocus()
|
self.ui.nameEdit.setFocus()
|
||||||
@@ -216,7 +190,7 @@ class PortOptionsDialog(QDialog):
|
|||||||
|
|
||||||
def accept(self) -> None:
|
def accept(self) -> None:
|
||||||
self._store_current()
|
self._store_current()
|
||||||
if any(not port.name.strip() for port, _orientation in self.ports):
|
if any(not port.name.strip() for port in self.ports):
|
||||||
QMessageBox.warning(self, "Invalid port", "Every port must have a name.")
|
QMessageBox.warning(self, "Invalid port", "Every port must have a name.")
|
||||||
return
|
return
|
||||||
super().accept()
|
super().accept()
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ class TextDefinitionEditor(QWidget):
|
|||||||
return self.ui.initialEquationsEdit.toPlainText()
|
return self.ui.initialEquationsEdit.toPlainText()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ports(self) -> tuple[list[Port], list[Port]]:
|
def ports(self) -> list[Port]:
|
||||||
return self.ui.portEditor.inputs, self.ui.portEditor.outputs
|
return self.ui.portEditor.ports
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parameters(self) -> list[Parameter]:
|
def parameters(self) -> list[Parameter]:
|
||||||
@@ -55,17 +55,16 @@ class TextDefinitionEditor(QWidget):
|
|||||||
declarations: str,
|
declarations: str,
|
||||||
initial_equations: str,
|
initial_equations: str,
|
||||||
equations: str,
|
equations: str,
|
||||||
inputs: list[Port],
|
ports: list[Port],
|
||||||
outputs: list[Port],
|
|
||||||
parameters: list[Parameter],
|
parameters: list[Parameter],
|
||||||
) -> None:
|
) -> None:
|
||||||
self._loading = True
|
self._loading = True
|
||||||
self.ui.declarationsEdit.setPlainText(declarations)
|
self.ui.declarationsEdit.setPlainText(declarations)
|
||||||
self.ui.initialEquationsEdit.setPlainText(initial_equations)
|
self.ui.initialEquationsEdit.setPlainText(initial_equations)
|
||||||
self.ui.equationsEdit.setPlainText(equations)
|
self.ui.equationsEdit.setPlainText(equations)
|
||||||
self.ui.portEditor.set_ports(inputs, outputs)
|
self.ui.portEditor.set_ports(ports)
|
||||||
self.ui.parameterEditor.set_parameters(parameters)
|
self.ui.parameterEditor.set_parameters(parameters)
|
||||||
self._set_editor_symbols(inputs, outputs, parameters)
|
self._set_editor_symbols(ports, parameters)
|
||||||
self._loading = False
|
self._loading = False
|
||||||
self.set_modified(False)
|
self.set_modified(False)
|
||||||
|
|
||||||
@@ -85,18 +84,20 @@ class TextDefinitionEditor(QWidget):
|
|||||||
self._mark_modified()
|
self._mark_modified()
|
||||||
|
|
||||||
def _refresh_editor_symbols(self) -> None:
|
def _refresh_editor_symbols(self) -> None:
|
||||||
inputs, outputs = self.ports
|
self._set_editor_symbols(self.ports, self.parameters)
|
||||||
self._set_editor_symbols(inputs, outputs, self.parameters)
|
|
||||||
|
|
||||||
def _set_editor_symbols(
|
def _set_editor_symbols(
|
||||||
self,
|
self,
|
||||||
inputs: list[Port],
|
ports: list[Port],
|
||||||
outputs: list[Port],
|
|
||||||
parameters: list[Parameter],
|
parameters: list[Parameter],
|
||||||
) -> None:
|
) -> None:
|
||||||
self._apply_editor_symbols(
|
self._apply_editor_symbols(
|
||||||
[port.name for port in inputs],
|
[
|
||||||
[port.name for port in outputs],
|
port.name
|
||||||
|
for port in ports
|
||||||
|
if port.orientation in {"input", "indifferent"}
|
||||||
|
],
|
||||||
|
[port.name for port in ports if port.orientation == "output"],
|
||||||
[parameter.name for parameter in parameters],
|
[parameter.name for parameter in parameters],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -458,8 +458,7 @@ class IconEditorDialog(QDialog):
|
|||||||
self.ui.setupUi(self)
|
self.ui.setupUi(self)
|
||||||
self.setWindowTitle(f"Icon Editor — {component.name}")
|
self.setWindowTitle(f"Icon Editor — {component.name}")
|
||||||
self.icon = Icon.from_dict(component.icon.to_dict())
|
self.icon = Icon.from_dict(component.icon.to_dict())
|
||||||
self.inputs = deepcopy(component.inputs)
|
self.ports = deepcopy(component.ports)
|
||||||
self.outputs = deepcopy(component.outputs)
|
|
||||||
self.tool_group = QButtonGroup(self)
|
self.tool_group = QButtonGroup(self)
|
||||||
self.tool_group.setExclusive(True)
|
self.tool_group.setExclusive(True)
|
||||||
self.tool_group.addButton(self.ui.pointerButton)
|
self.tool_group.addButton(self.ui.pointerButton)
|
||||||
@@ -485,8 +484,10 @@ class IconEditorDialog(QDialog):
|
|||||||
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100)
|
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100)
|
||||||
for element in self.icon.elements:
|
for element in self.icon.elements:
|
||||||
self.scene.addItem(ShapeItem(element))
|
self.scene.addItem(ShapeItem(element))
|
||||||
self._add_ports(self.inputs, "input", 0.0)
|
input_side = [port for port in self.ports if port.orientation != "output"]
|
||||||
self._add_ports(self.outputs, "output", self.icon.width)
|
output_side = [port for port in self.ports if port.orientation == "output"]
|
||||||
|
self._add_ports(input_side, "input", 0.0)
|
||||||
|
self._add_ports(output_side, "output", self.icon.width)
|
||||||
self._initial_fit_pending = True
|
self._initial_fit_pending = True
|
||||||
|
|
||||||
def showEvent(self, event) -> None: # noqa: N802 (Qt API name)
|
def showEvent(self, event) -> None: # noqa: N802 (Qt API name)
|
||||||
|
|||||||
@@ -173,8 +173,16 @@ class ComponentGraphicsItem(QGraphicsObject):
|
|||||||
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
|
||||||
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||||
)
|
)
|
||||||
self.input_ports = self._create_ports(component.inputs, "target", 0.0)
|
self.input_ports = self._create_ports(
|
||||||
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
|
[port for port in component.ports if port.orientation != "output"],
|
||||||
|
"target",
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
self.output_ports = self._create_ports(
|
||||||
|
[port for port in component.ports if port.orientation == "output"],
|
||||||
|
"source",
|
||||||
|
self.WIDTH,
|
||||||
|
)
|
||||||
self.setTransformOriginPoint(self.hitbox.center())
|
self.setTransformOriginPoint(self.hitbox.center())
|
||||||
self.setRotation(component.rotation)
|
self.setRotation(component.rotation)
|
||||||
self.name_label: NameLabelItem | None = None
|
self.name_label: NameLabelItem | None = None
|
||||||
@@ -371,8 +379,10 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.connection_id = connection.id
|
self.connection_id = connection.id
|
||||||
self.name = connection.name
|
self.name = connection.name
|
||||||
|
self.connection_type = connection.type
|
||||||
self.source_is_junction = connection.source.junction is not None
|
self.source_is_junction = connection.source.junction is not None
|
||||||
self.target_is_junction = connection.target.junction is not None
|
self.target_is_junction = connection.target.junction is not None
|
||||||
|
self.causality = connection.causality
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.style = style or ConnectionStyle()
|
self.style = style or ConnectionStyle()
|
||||||
self.start = QPointF()
|
self.start = QPointF()
|
||||||
@@ -417,7 +427,11 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
|||||||
self.setSelected(True)
|
self.setSelected(True)
|
||||||
menu = QMenu()
|
menu = QMenu()
|
||||||
add_node_action = menu.addAction("Add Node")
|
add_node_action = menu.addAction("Add Node")
|
||||||
add_junction_action = menu.addAction("Add Junction")
|
add_junction_action = (
|
||||||
|
menu.addAction("Add Junction")
|
||||||
|
if self.connection_type != "power"
|
||||||
|
else None
|
||||||
|
)
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
options_action = menu.addAction("Connection Options…")
|
options_action = menu.addAction("Connection Options…")
|
||||||
selected = menu.exec(event.screenPos())
|
selected = menu.exec(event.screenPos())
|
||||||
@@ -425,7 +439,7 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
|||||||
scene = self.scene()
|
scene = self.scene()
|
||||||
if isinstance(scene, GraphScene):
|
if isinstance(scene, GraphScene):
|
||||||
scene.add_route_node("connection", self.connection_id, event.scenePos())
|
scene.add_route_node("connection", self.connection_id, event.scenePos())
|
||||||
elif selected is add_junction_action:
|
elif add_junction_action is not None and selected is add_junction_action:
|
||||||
scene = self.scene()
|
scene = self.scene()
|
||||||
if isinstance(scene, GraphScene):
|
if isinstance(scene, GraphScene):
|
||||||
scene.add_connection_junction(self.connection_id, event.scenePos())
|
scene.add_connection_junction(self.connection_id, event.scenePos())
|
||||||
@@ -512,12 +526,36 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
|||||||
if self.style.arrow_style == "open":
|
if self.style.arrow_style == "open":
|
||||||
painter.drawLine(tip, right)
|
painter.drawLine(tip, right)
|
||||||
|
|
||||||
|
def _draw_causality_mark(
|
||||||
|
self, painter: QPainter, point: QPointF, direction: QPointF, *, warning: bool
|
||||||
|
) -> None:
|
||||||
|
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5)
|
||||||
|
normal = QPointF(-direction.y() / length, direction.x() / length)
|
||||||
|
half_length = 6.0
|
||||||
|
color = QColor("#c25a00") if warning else self.pen().color()
|
||||||
|
painter.setPen(QPen(color, max(2.0, self.pen().widthF()), Qt.PenStyle.SolidLine))
|
||||||
|
painter.drawLine(point - normal * half_length, point + normal * half_length)
|
||||||
|
|
||||||
def paint(self, painter: QPainter, option, widget=None) -> None:
|
def paint(self, painter: QPainter, option, widget=None) -> None:
|
||||||
super().paint(painter, option, widget)
|
super().paint(painter, option, widget)
|
||||||
if self.style.arrow_at_target and not self.target_is_junction:
|
if self.style.arrow_at_target and not self.target_is_junction:
|
||||||
self._draw_arrow(painter, self.end, self.end_direction)
|
self._draw_arrow(painter, self.end, self.end_direction)
|
||||||
if self.style.arrow_at_source and not self.source_is_junction:
|
if self.style.arrow_at_source and not self.source_is_junction:
|
||||||
self._draw_arrow(painter, self.start, -self.start_direction)
|
self._draw_arrow(painter, self.start, -self.start_direction)
|
||||||
|
if self.causality in {"source", "warn_source"}:
|
||||||
|
self._draw_causality_mark(
|
||||||
|
painter,
|
||||||
|
self.start,
|
||||||
|
self.start_direction,
|
||||||
|
warning=self.causality == "warn_source",
|
||||||
|
)
|
||||||
|
elif self.causality in {"target", "warn_target"}:
|
||||||
|
self._draw_causality_mark(
|
||||||
|
painter,
|
||||||
|
self.end,
|
||||||
|
self.end_direction,
|
||||||
|
warning=self.causality == "warn_target",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WaypointHandle(QGraphicsEllipseItem):
|
class WaypointHandle(QGraphicsEllipseItem):
|
||||||
@@ -898,12 +936,12 @@ class GraphScene(QGraphicsScene):
|
|||||||
owner = self.controller.active_component
|
owner = self.controller.active_component
|
||||||
if owner is None or owner.implementation_kind != "graph":
|
if owner is None or owner.implementation_kind != "graph":
|
||||||
return
|
return
|
||||||
for port in owner.inputs:
|
for port in (port for port in owner.ports if port.orientation != "output"):
|
||||||
item = InterfaceTerminalItem(port, "input", self.controller)
|
item = InterfaceTerminalItem(port, "input", self.controller)
|
||||||
self.addItem(item)
|
self.addItem(item)
|
||||||
item.setPos(port.x, port.y)
|
item.setPos(port.x, port.y)
|
||||||
self.input_items[port.id] = item
|
self.input_items[port.id] = item
|
||||||
for port in owner.outputs:
|
for port in (port for port in owner.ports if port.orientation == "output"):
|
||||||
item = InterfaceTerminalItem(port, "output", self.controller)
|
item = InterfaceTerminalItem(port, "output", self.controller)
|
||||||
self.addItem(item)
|
self.addItem(item)
|
||||||
item.setPos(port.x, port.y)
|
item.setPos(port.x, port.y)
|
||||||
@@ -1224,15 +1262,16 @@ class GraphScene(QGraphicsScene):
|
|||||||
source_item: ComponentGraphicsItem, target_item: ComponentGraphicsItem
|
source_item: ComponentGraphicsItem, target_item: ComponentGraphicsItem
|
||||||
) -> None:
|
) -> None:
|
||||||
source_ports = [
|
source_ports = [
|
||||||
*source_item.component.outputs,
|
|
||||||
*(
|
|
||||||
port
|
port
|
||||||
for port in source_item.component.inputs
|
for port in source_item.component.ports
|
||||||
if port.orientation == "indifferent"
|
if port.orientation in {"output", "indifferent"}
|
||||||
),
|
|
||||||
]
|
]
|
||||||
for output in source_ports:
|
for output in source_ports:
|
||||||
for input_port in target_item.component.inputs:
|
for input_port in (
|
||||||
|
port
|
||||||
|
for port in target_item.component.ports
|
||||||
|
if port.orientation in {"input", "indifferent"}
|
||||||
|
):
|
||||||
if not PortTypeRegistry.compatible(
|
if not PortTypeRegistry.compatible(
|
||||||
output.type,
|
output.type,
|
||||||
input_port.type,
|
input_port.type,
|
||||||
@@ -1267,7 +1306,11 @@ class GraphScene(QGraphicsScene):
|
|||||||
choices: list[ConnectionChoice] = []
|
choices: list[ConnectionChoice] = []
|
||||||
interface = Endpoint(interface=terminal.port.id)
|
interface = Endpoint(interface=terminal.port.id)
|
||||||
if terminal.direction == "input":
|
if terminal.direction == "input":
|
||||||
for port in component.component.inputs:
|
for port in (
|
||||||
|
port
|
||||||
|
for port in component.component.ports
|
||||||
|
if port.orientation in {"input", "indifferent"}
|
||||||
|
):
|
||||||
target = Endpoint(block=component.component_id, port=port.id)
|
target = Endpoint(block=component.component_id, port=port.id)
|
||||||
if not PortTypeRegistry.compatible(
|
if not PortTypeRegistry.compatible(
|
||||||
terminal.port.type, port.type, terminal.port.domain, port.domain
|
terminal.port.type, port.type, terminal.port.domain, port.domain
|
||||||
@@ -1286,12 +1329,9 @@ class GraphScene(QGraphicsScene):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ports = [
|
ports = [
|
||||||
*component.component.outputs,
|
|
||||||
*(
|
|
||||||
port
|
port
|
||||||
for port in component.component.inputs
|
for port in component.component.ports
|
||||||
if port.orientation == "indifferent"
|
if port.orientation in {"output", "indifferent"}
|
||||||
),
|
|
||||||
]
|
]
|
||||||
for port in ports:
|
for port in ports:
|
||||||
source = Endpoint(block=component.component_id, port=port.id)
|
source = Endpoint(block=component.component_id, port=port.id)
|
||||||
@@ -1319,7 +1359,11 @@ class GraphScene(QGraphicsScene):
|
|||||||
) -> list[ConnectionChoice]:
|
) -> list[ConnectionChoice]:
|
||||||
choices: list[ConnectionChoice] = []
|
choices: list[ConnectionChoice] = []
|
||||||
source = junction.endpoint
|
source = junction.endpoint
|
||||||
for port in component.component.inputs:
|
for port in (
|
||||||
|
port
|
||||||
|
for port in component.component.ports
|
||||||
|
if port.orientation in {"input", "indifferent"}
|
||||||
|
):
|
||||||
target = Endpoint(block=component.component_id, port=port.id)
|
target = Endpoint(block=component.component_id, port=port.id)
|
||||||
if not PortTypeRegistry.compatible(junction.junction.type, port.type):
|
if not PortTypeRegistry.compatible(junction.junction.type, port.type):
|
||||||
continue
|
continue
|
||||||
@@ -1600,6 +1644,8 @@ class GraphScene(QGraphicsScene):
|
|||||||
graphics = self.connection_items.get(connection_id)
|
graphics = self.connection_items.get(connection_id)
|
||||||
if connection is None or graphics is None:
|
if connection is None or graphics is None:
|
||||||
return
|
return
|
||||||
|
if connection.type == "power":
|
||||||
|
return
|
||||||
snapped = _snapped(position)
|
snapped = _snapped(position)
|
||||||
anchors = [graphics.start, *points, graphics.end]
|
anchors = [graphics.start, *points, graphics.end]
|
||||||
|
|
||||||
@@ -1850,8 +1896,7 @@ class GraphWorkspaceView(QGraphicsView):
|
|||||||
return
|
return
|
||||||
blocks: set[str] = set()
|
blocks: set[str] = set()
|
||||||
connections: set[str] = set()
|
connections: set[str] = set()
|
||||||
inputs: set[str] = set()
|
ports: set[str] = set()
|
||||||
outputs: set[str] = set()
|
|
||||||
annotations: set[str] = set()
|
annotations: set[str] = set()
|
||||||
for item in self.scene().selectedItems():
|
for item in self.scene().selectedItems():
|
||||||
if isinstance(item, ComponentGraphicsItem):
|
if isinstance(item, ComponentGraphicsItem):
|
||||||
@@ -1859,10 +1904,10 @@ class GraphWorkspaceView(QGraphicsView):
|
|||||||
elif isinstance(item, ConnectionGraphicsItem):
|
elif isinstance(item, ConnectionGraphicsItem):
|
||||||
connections.add(item.connection_id)
|
connections.add(item.connection_id)
|
||||||
elif isinstance(item, InterfaceTerminalItem):
|
elif isinstance(item, InterfaceTerminalItem):
|
||||||
(inputs if item.direction == "input" else outputs).add(item.port.id)
|
ports.add(item.port.id)
|
||||||
elif isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem)):
|
elif isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem)):
|
||||||
annotations.add(item.annotation_id)
|
annotations.add(item.annotation_id)
|
||||||
self.controller.delete_selection(blocks, connections, inputs, outputs)
|
self.controller.delete_selection(blocks, connections, ports)
|
||||||
self.controller.delete_annotations(annotations)
|
self.controller.delete_annotations(annotations)
|
||||||
|
|
||||||
def has_selected_components(self) -> bool:
|
def has_selected_components(self) -> bool:
|
||||||
|
|||||||
@@ -540,8 +540,7 @@ class MainWindow(QMainWindow):
|
|||||||
component.source.get("declarations", ""),
|
component.source.get("declarations", ""),
|
||||||
component.source.get("initialEquations", ""),
|
component.source.get("initialEquations", ""),
|
||||||
component.source.get("equations", ""),
|
component.source.get("equations", ""),
|
||||||
component.inputs,
|
component.ports,
|
||||||
component.outputs,
|
|
||||||
component.parameters,
|
component.parameters,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -569,12 +568,11 @@ class MainWindow(QMainWindow):
|
|||||||
@Slot()
|
@Slot()
|
||||||
def apply_text_definition(self) -> bool:
|
def apply_text_definition(self) -> bool:
|
||||||
try:
|
try:
|
||||||
inputs, outputs = self.ui.textDefinitionEditor.ports
|
ports = self.ui.textDefinitionEditor.ports
|
||||||
self._applying_text_definition = True
|
self._applying_text_definition = True
|
||||||
try:
|
try:
|
||||||
self.document_controller.replace_active_text_definition(
|
self.document_controller.replace_active_text_definition(
|
||||||
inputs,
|
ports,
|
||||||
outputs,
|
|
||||||
self.ui.textDefinitionEditor.declarations,
|
self.ui.textDefinitionEditor.declarations,
|
||||||
self.ui.textDefinitionEditor.initial_equations,
|
self.ui.textDefinitionEditor.initial_equations,
|
||||||
self.ui.textDefinitionEditor.equations,
|
self.ui.textDefinitionEditor.equations,
|
||||||
@@ -643,12 +641,23 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
if not filename:
|
if not filename:
|
||||||
return
|
return
|
||||||
|
self.open_document_path(Path(filename), check_unsaved=False)
|
||||||
|
|
||||||
|
def open_document_path(self, path: Path, *, check_unsaved: bool = True) -> bool:
|
||||||
|
"""Open a document path selected by the UI or supplied at startup."""
|
||||||
|
|
||||||
|
if check_unsaved and (
|
||||||
|
not self._resolve_source_edits() or not self._maybe_save()
|
||||||
|
):
|
||||||
|
return False
|
||||||
try:
|
try:
|
||||||
self.document_controller.load(Path(filename))
|
self.document_controller.load(path)
|
||||||
self.log.info("Opened document %s", filename)
|
self.log.info("Opened document %s", path)
|
||||||
except (OSError, ValueError) as error:
|
except (OSError, ValueError) as error:
|
||||||
self.log.error("Could not open document %s: %s", filename, error)
|
self.log.error("Could not open document %s: %s", path, error)
|
||||||
QMessageBox.critical(self, "Could not open graph", str(error))
|
QMessageBox.critical(self, "Could not open graph", str(error))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
@Slot()
|
@Slot()
|
||||||
def save_document(self) -> bool:
|
def save_document(self) -> bool:
|
||||||
@@ -818,9 +827,8 @@ class MainWindow(QMainWindow):
|
|||||||
elif selected is ports_action:
|
elif selected is ports_action:
|
||||||
dialog = PortOptionsDialog(component, self)
|
dialog = PortOptionsDialog(component, self)
|
||||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||||
old_inputs, old_outputs = deepcopy(component.inputs), deepcopy(component.outputs)
|
old_ports = deepcopy(component.ports)
|
||||||
component.inputs = dialog.inputs
|
component.ports = dialog.ports
|
||||||
component.outputs = dialog.outputs
|
|
||||||
library = next(
|
library = next(
|
||||||
(
|
(
|
||||||
library
|
library
|
||||||
@@ -834,7 +842,7 @@ class MainWindow(QMainWindow):
|
|||||||
library.document.validate()
|
library.document.validate()
|
||||||
DocumentSerializer.save(library.document, Path(library.source_path))
|
DocumentSerializer.save(library.document, Path(library.source_path))
|
||||||
except (OSError, ValueError) as error:
|
except (OSError, ValueError) as error:
|
||||||
component.inputs, component.outputs = old_inputs, old_outputs
|
component.ports = old_ports
|
||||||
self.log.error("Could not change library ports: %s", error)
|
self.log.error("Could not change library ports: %s", error)
|
||||||
QMessageBox.warning(self, "Cannot change library ports", str(error))
|
QMessageBox.warning(self, "Cannot change library ports", str(error))
|
||||||
self.library_tree_model.rebuild()
|
self.library_tree_model.rebuild()
|
||||||
@@ -876,7 +884,7 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self.document_controller.edit_component_ports(
|
self.document_controller.edit_component_ports(
|
||||||
component_id, dialog.inputs, dialog.outputs
|
component_id, dialog.ports
|
||||||
)
|
)
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
self.log.error("Could not change component ports: %s", error)
|
self.log.error("Could not change component ports: %s", error)
|
||||||
@@ -912,8 +920,7 @@ class MainWindow(QMainWindow):
|
|||||||
component_id,
|
component_id,
|
||||||
dialog.ui.nameEdit.text().strip(),
|
dialog.ui.nameEdit.text().strip(),
|
||||||
dialog.edited_icon,
|
dialog.edited_icon,
|
||||||
dialog.edited_inputs,
|
dialog.edited_ports,
|
||||||
dialog.edited_outputs,
|
|
||||||
dialog.ui.showSubtreeCheckBox.isChecked(),
|
dialog.ui.showSubtreeCheckBox.isChecked(),
|
||||||
dialog.ui.showNameCheckBox.isChecked(),
|
dialog.ui.showNameCheckBox.isChecked(),
|
||||||
)
|
)
|
||||||
@@ -926,8 +933,7 @@ class MainWindow(QMainWindow):
|
|||||||
owner = self.document_controller.active_component
|
owner = self.document_controller.active_component
|
||||||
if owner is None:
|
if owner is None:
|
||||||
return
|
return
|
||||||
ports = owner.inputs if direction == "input" else owner.outputs
|
port = next((item for item in owner.ports if item.id == port_id), None)
|
||||||
port = next((item for item in ports if item.id == port_id), None)
|
|
||||||
if port is None:
|
if port is None:
|
||||||
return
|
return
|
||||||
dialog = ItemOptionsDialog(f"{direction.title()} Options", port.name, self)
|
dialog = ItemOptionsDialog(f"{direction.title()} Options", port.name, self)
|
||||||
|
|||||||
@@ -14,8 +14,7 @@
|
|||||||
},
|
},
|
||||||
"rotation": 0.0,
|
"rotation": 0.0,
|
||||||
"interface": {
|
"interface": {
|
||||||
"inputs": [],
|
"ports": []
|
||||||
"outputs": []
|
|
||||||
},
|
},
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"icon": {
|
"icon": {
|
||||||
@@ -69,7 +68,7 @@
|
|||||||
},
|
},
|
||||||
"rotation": 0.0,
|
"rotation": 0.0,
|
||||||
"interface": {
|
"interface": {
|
||||||
"inputs": [
|
"ports": [
|
||||||
{
|
{
|
||||||
"id": "port-2e1d884f",
|
"id": "port-2e1d884f",
|
||||||
"name": "p",
|
"name": "p",
|
||||||
@@ -96,9 +95,7 @@
|
|||||||
"orientation": "input",
|
"orientation": "input",
|
||||||
"domain": "power",
|
"domain": "power",
|
||||||
"causality": "preferred effort out"
|
"causality": "preferred effort out"
|
||||||
}
|
},
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
{
|
||||||
"id": "port-ca7716d5",
|
"id": "port-ca7716d5",
|
||||||
"name": "state",
|
"name": "state",
|
||||||
@@ -193,7 +190,7 @@
|
|||||||
},
|
},
|
||||||
"rotation": 0.0,
|
"rotation": 0.0,
|
||||||
"interface": {
|
"interface": {
|
||||||
"inputs": [
|
"ports": [
|
||||||
{
|
{
|
||||||
"id": "port-3e53bf80",
|
"id": "port-3e53bf80",
|
||||||
"name": "p",
|
"name": "p",
|
||||||
@@ -221,8 +218,7 @@
|
|||||||
"domain": "power",
|
"domain": "power",
|
||||||
"causality": "indifferent"
|
"causality": "indifferent"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"outputs": []
|
|
||||||
},
|
},
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"icon": {
|
"icon": {
|
||||||
@@ -275,7 +271,7 @@
|
|||||||
},
|
},
|
||||||
"rotation": 0.0,
|
"rotation": 0.0,
|
||||||
"interface": {
|
"interface": {
|
||||||
"inputs": [
|
"ports": [
|
||||||
{
|
{
|
||||||
"id": "port-2e1d884f",
|
"id": "port-2e1d884f",
|
||||||
"name": "p",
|
"name": "p",
|
||||||
@@ -303,8 +299,7 @@
|
|||||||
"domain": "power",
|
"domain": "power",
|
||||||
"causality": "indifferent"
|
"causality": "indifferent"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"outputs": []
|
|
||||||
},
|
},
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -371,8 +366,7 @@
|
|||||||
},
|
},
|
||||||
"rotation": 0.0,
|
"rotation": 0.0,
|
||||||
"interface": {
|
"interface": {
|
||||||
"inputs": [],
|
"ports": [
|
||||||
"outputs": [
|
|
||||||
{
|
{
|
||||||
"id": "port-2e1d884f",
|
"id": "port-2e1d884f",
|
||||||
"name": "p",
|
"name": "p",
|
||||||
@@ -473,7 +467,9 @@
|
|||||||
"name": "",
|
"name": "",
|
||||||
"properties": {
|
"properties": {
|
||||||
"waypoints": []
|
"waypoints": []
|
||||||
}
|
},
|
||||||
|
"type": "power",
|
||||||
|
"causality": "none"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a73a99a1-54e2-461d-a9a7-2d6d11d500f4",
|
"id": "a73a99a1-54e2-461d-a9a7-2d6d11d500f4",
|
||||||
@@ -488,7 +484,9 @@
|
|||||||
"name": "",
|
"name": "",
|
||||||
"properties": {
|
"properties": {
|
||||||
"waypoints": []
|
"waypoints": []
|
||||||
}
|
},
|
||||||
|
"type": "power",
|
||||||
|
"causality": "none"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "dd5fb1e2-b125-4b5f-a792-dac89dd89ad3",
|
"id": "dd5fb1e2-b125-4b5f-a792-dac89dd89ad3",
|
||||||
@@ -503,7 +501,9 @@
|
|||||||
"name": "",
|
"name": "",
|
||||||
"properties": {
|
"properties": {
|
||||||
"waypoints": []
|
"waypoints": []
|
||||||
}
|
},
|
||||||
|
"type": "power",
|
||||||
|
"causality": "none"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"annotations": [],
|
"annotations": [],
|
||||||
|
|||||||
Reference in New Issue
Block a user