Bond graph ports and drawing added

This commit is contained in:
2026-07-22 12:24:58 +02:00
parent 0c578af85d
commit 09248421d0
12 changed files with 614 additions and 165 deletions

View File

@@ -59,7 +59,12 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
- Port orientation is presented as one unified list in the UI, while the model - Port orientation is presented as one unified list in the UI, while the model
indexes inputs and outputs separately for connection semantics. indexes inputs and outputs separately for connection semantics.
- 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` is currently the only type. connected. Signal ports connect by type; power ports additionally require the
same domain. Editable power domains and causalities live in
`core/power_domains.py`.
- Power ports may use `indifferent` orientation and can act as either connection
endpoint. For two indifferent ports, click order determines source/arrow
direction; otherwise output/input semantics determine direction.
- Port connector type and signal value type are separate. Signal ports and - Port connector type and signal value type are separate. Signal ports and
parameters carry editable value type, quantity, unit, row/column dimensions, parameters carry editable value type, quantity, unit, row/column dimensions,
and description metadata. Editable quantity/unit suggestions live in and description metadata. Editable quantity/unit suggestions live in
@@ -78,7 +83,8 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
intersection of the owning hitbox and its center-to-adjacent-route-point ray. intersection of the owning hitbox and its center-to-adjacent-route-point ray.
- Connection appearance is configured per port type in - Connection appearance is configured per port type in
`gui/graphics/connection_styles.py`, including color, width, pen style, and `gui/graphics/connection_styles.py`, including color, width, pen style, and
source/target arrowheads. Do not scatter those constants through painters. source/target arrowheads. Arrow styles are `open`, `half`, or `filled`. Do not
scatter those constants through painters.
- Graph annotations are `box`, `line`, or `text` objects in `Graph.annotations`. - Graph annotations are `box`, `line`, or `text` objects in `Graph.annotations`.
They use integer layers below or above graph layer 0. Annotation lines reuse They use integer layers below or above graph layer 0. Annotation lines reuse
connection polyline and absolute `properties.waypoints` semantics. Graph connection polyline and absolute `properties.waypoints` semantics. Graph

View File

@@ -6,6 +6,7 @@ from typing import Any
from uuid import uuid4 from uuid import uuid4
from bedit.core.port_types import PortTypeRegistry from bedit.core.port_types import PortTypeRegistry
from bedit.core.power_domains import POWER_CAUSALITIES, POWER_DOMAINS
def _dimensions(data: Any, subject: str) -> tuple[int, int]: def _dimensions(data: Any, subject: str) -> tuple[int, int]:
@@ -36,6 +37,9 @@ class Port:
rows: int = 1 rows: int = 1
columns: int = 1 columns: int = 1
description: str = "" description: str = ""
orientation: str = "input"
domain: str = "power"
causality: str = "indifferent"
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
return { return {
@@ -50,10 +54,13 @@ class Port:
"unit": self.unit, "unit": self.unit,
"dimensions": {"rows": self.rows, "columns": self.columns}, "dimensions": {"rows": self.rows, "columns": self.columns},
"description": self.description, "description": self.description,
"orientation": self.orientation,
"domain": self.domain,
"causality": self.causality,
} }
@classmethod @classmethod
def from_dict(cls, data: dict[str, Any]) -> "Port": def from_dict(cls, data: dict[str, Any], default_orientation: str = "input") -> "Port":
position = data.get("position", {}) position = data.get("position", {})
rows, columns = _dimensions(data.get("dimensions", {}), "Port") rows, columns = _dimensions(data.get("dimensions", {}), "Port")
return cls( return cls(
@@ -70,6 +77,9 @@ class Port:
rows=rows, rows=rows,
columns=columns, columns=columns,
description=str(data.get("description", "")), description=str(data.get("description", "")),
orientation=str(data.get("orientation", default_orientation)),
domain=str(data.get("domain", "power")),
causality=str(data.get("causality", "indifferent")),
) )
@@ -417,8 +427,8 @@ 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) for item in interface.get("inputs", [])], inputs=[Port.from_dict(item, "input") for item in interface.get("inputs", [])],
outputs=[Port.from_dict(item) for item in interface.get("outputs", [])], 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", {})),
@@ -528,6 +538,15 @@ class GraphDocument:
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.inputs, *owner.outputs):
PortTypeRegistry.get(port.type) PortTypeRegistry.get(port.type)
if port.orientation not in {"input", "output", "indifferent"}:
raise ValueError(f"Port {port.name!r} has an invalid orientation")
if port.orientation == "indifferent" and port.type != "power":
raise ValueError("Only power ports may have indifferent orientation")
if port.type == "power":
if port.domain not in {domain.id for domain in POWER_DOMAINS}:
raise ValueError(f"Power port {port.name!r} has an unknown domain")
if port.causality not in POWER_CAUSALITIES:
raise ValueError(f"Power port {port.name!r} has invalid causality")
if not port.value_type.strip(): if not port.value_type.strip():
raise ValueError(f"Port {port.name!r} has no value type") raise ValueError(f"Port {port.name!r} has no value type")
if port.rows < 1 or port.columns < 1: if port.rows < 1 or port.columns < 1:
@@ -549,9 +568,17 @@ class GraphDocument:
source_port = next(p for p in owner.inputs if p.id == connection.source.interface) source_port = next(p for p in owner.inputs if p.id == connection.source.interface)
else: else:
source = owner.graph.blocks.get(connection.source.block or "") source = owner.graph.blocks.get(connection.source.block or "")
if source is None or connection.source.port not in {p.id for p in source.outputs}: source_ports = (
[]
if source is None
else [
*source.outputs,
*(p for p in source.inputs if p.orientation == "indifferent"),
]
)
if source is None or connection.source.port not in {p.id for p in source_ports}:
raise ValueError(f"Connection {connection.id} uses an unknown block output") raise ValueError(f"Connection {connection.id} uses an unknown block output")
source_port = next(p for p in source.outputs if p.id == connection.source.port) source_port = next(p for p in source_ports if p.id == connection.source.port)
if connection.target.junction is not None: if connection.target.junction is not None:
junction = owner.graph.junctions.get(connection.target.junction) junction = owner.graph.junctions.get(connection.target.junction)
if junction is None: if junction is None:
@@ -565,15 +592,27 @@ class GraphDocument:
allows_multiple_connections=False, allows_multiple_connections=False,
) )
elif connection.target.interface is not None: elif connection.target.interface is not None:
if connection.target.interface not in output_ids: indifferent_ids = {
port.id for port in owner.inputs if port.orientation == "indifferent"
}
if connection.target.interface not in output_ids | indifferent_ids:
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(p for p in owner.outputs if p.id == connection.target.interface) target_port = next(
p
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}: if target is None or connection.target.port not in {p.id for p in target.inputs}:
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(p for p in target.inputs if p.id == connection.target.port)
if not PortTypeRegistry.compatible(source_port.type, target_port.type): if not PortTypeRegistry.compatible(
source_port.type,
target_port.type,
source_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")
source_key = ( source_key = (
"source-junction" "source-junction"
@@ -670,27 +709,11 @@ def clone_component(source: Component) -> Component:
x=current.x, x=current.x,
y=current.y, y=current.y,
inputs=[ inputs=[
Port( Port.from_dict(port.to_dict(), "input")
port.id,
port.name,
port.x,
port.y,
deepcopy(port.properties),
port.type,
port.allows_multiple_connections,
)
for port in current.inputs for port in current.inputs
], ],
outputs=[ outputs=[
Port( Port.from_dict(port.to_dict(), "output")
port.id,
port.name,
port.x,
port.y,
deepcopy(port.properties),
port.type,
port.allows_multiple_connections,
)
for port in current.outputs for port in current.outputs
], ],
parameters=deepcopy(current.parameters), parameters=deepcopy(current.parameters),

View File

@@ -14,6 +14,7 @@ class PortType:
class PortTypeRegistry: class PortTypeRegistry:
_types = { _types = {
"signal": PortType("signal", "Signal", "A scalar signal connection"), "signal": PortType("signal", "Signal", "A scalar signal connection"),
"power": PortType("power", "Power", "A two-variable power connection"),
} }
@classmethod @classmethod
@@ -28,5 +29,13 @@ class PortTypeRegistry:
raise ValueError(f"Unknown port type: {type_id}") from error raise ValueError(f"Unknown port type: {type_id}") from error
@classmethod @classmethod
def compatible(cls, first: str, second: str) -> bool: def compatible(
return cls.get(first).accepts(cls.get(second)) cls,
first: str,
second: str,
first_domain: str = "",
second_domain: str = "",
) -> bool:
if not cls.get(first).accepts(cls.get(second)):
return False
return first != "power" or first_domain == second_domain

View File

@@ -0,0 +1,31 @@
"""Editable power-port domain definitions used by the port editor."""
from dataclasses import dataclass
@dataclass(frozen=True)
class PowerDomain:
id: str
display_name: str
effort: str
flow: str
POWER_DOMAINS: tuple[PowerDomain, ...] = (
PowerDomain("power", "Power", "p.e", "p.f"),
)
def power_domain(domain_id: str) -> PowerDomain:
return next((domain for domain in POWER_DOMAINS if domain.id == domain_id), POWER_DOMAINS[0])
POWER_CAUSALITIES = (
"fixed flow out",
"fixed effort out",
"preferred flow out",
"preferred effort out",
"likes flow out",
"likes effort out",
"indifferent",
)

View File

@@ -285,7 +285,12 @@ class DocumentController(QObject):
target_port = self._port_for_endpoint(target, "target") target_port = self._port_for_endpoint(target, "target")
if source_port is None or target_port is None: if source_port is None or target_port is None:
raise ValueError("A connection endpoint no longer exists") raise ValueError("A connection endpoint no longer exists")
if not PortTypeRegistry.compatible(source_port.type, target_port.type): if not PortTypeRegistry.compatible(
source_port.type,
target_port.type,
source_port.domain,
target_port.domain,
):
raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}") raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}")
if not self.endpoint_accepts_connection(source, "source"): if not self.endpoint_accepts_connection(source, "source"):
raise ValueError( raise ValueError(
@@ -573,11 +578,24 @@ class DocumentController(QObject):
) )
if endpoint.interface is not None: if endpoint.interface is not None:
ports = owner.inputs if role == "source" else owner.outputs ports = owner.inputs if role == "source" else owner.outputs
ports = [
*ports,
*(
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 ports = component.outputs if role == "source" else component.inputs
if role == "source":
ports = [
*ports,
*(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
) )
@@ -664,6 +682,9 @@ class DocumentController(QObject):
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] input_ids = [port.id for port in inputs]
output_ids = [port.id for port in outputs] output_ids = [port.id for port in outputs]
source_ids = output_ids + [
port.id for port in inputs if port.orientation == "indifferent"
]
if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids): if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids):
raise ValueError("Input and output IDs must be unique") raise ValueError("Input and output IDs must be unique")
if any(not port.name.strip() for port in (*inputs, *outputs)): if any(not port.name.strip() for port in (*inputs, *outputs)):
@@ -686,7 +707,7 @@ class DocumentController(QObject):
) )
if ( if (
connection.source.block == component.id connection.source.block == component.id
and connection.source.port not in output_ids and connection.source.port not in source_ids
): ):
raise ValueError( raise ValueError(
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"
@@ -842,6 +863,9 @@ class DocumentController(QObject):
return return
input_ids = {port.id for port in inputs} input_ids = {port.id for port in inputs}
output_ids = {port.id for port in outputs} output_ids = {port.id for port in outputs}
source_ids = output_ids | {
port.id for port in inputs if port.orientation == "indifferent"
}
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():
@@ -852,7 +876,7 @@ class DocumentController(QObject):
raise ValueError("An input cannot be removed or reoriented while connected") raise ValueError("An input cannot be removed or reoriented while connected")
if ( if (
connection.source.block == component_id connection.source.block == component_id
and connection.source.port not in output_ids and connection.source.port not in source_ids
): ):
raise ValueError("An output cannot be removed or reoriented while connected") raise ValueError("An output cannot be removed or reoriented while connected")
for connection in component.graph.connections.values(): for connection in component.graph.connections.values():

View File

@@ -6,6 +6,7 @@ from PySide6.QtWidgets import QDialog, QDialogButtonBox, QListWidgetItem, QMessa
from bedit.core.model import Component, Port from bedit.core.model import Component, Port
from bedit.core.physical_types import QUANTITIES, UNITS from bedit.core.physical_types import QUANTITIES, UNITS
from bedit.core.power_domains import POWER_CAUSALITIES, POWER_DOMAINS, power_domain
from bedit.core.port_types import PortTypeRegistry from bedit.core.port_types import PortTypeRegistry
from bedit.gui.generated.ui_port_options_dialog import Ui_PortOptionsDialog from bedit.gui.generated.ui_port_options_dialog import Ui_PortOptionsDialog
@@ -13,6 +14,13 @@ 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."""
@@ -25,8 +33,8 @@ class PortOptionsDialog(QDialog):
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: list[tuple[Port, str]] = [
*((deepcopy(port), "input") for port in component.inputs), *((_oriented_copy(port, "input"), port.orientation or "input") for port in component.inputs),
*((deepcopy(port), "output") for port in component.outputs), *((_oriented_copy(port, "output"), "output") for port in component.outputs),
] ]
self._loading = False self._loading = False
self.ui.typeCombo.clear() self.ui.typeCombo.clear()
@@ -36,6 +44,10 @@ class PortOptionsDialog(QDialog):
self.ui.unitCombo.addItems(UNITS) self.ui.unitCombo.addItems(UNITS)
self.ui.orientationCombo.setItemData(0, "input") self.ui.orientationCombo.setItemData(0, "input")
self.ui.orientationCombo.setItemData(1, "output") self.ui.orientationCombo.setItemData(1, "output")
self.ui.orientationCombo.setItemData(2, "indifferent")
for domain in POWER_DOMAINS:
self.ui.domainCombo.addItem(domain.display_name, domain.id)
self.ui.causalityCombo.addItems(POWER_CAUSALITIES)
self.ui.portList.currentRowChanged.connect(self._load_current) self.ui.portList.currentRowChanged.connect(self._load_current)
self.ui.addPortButton.clicked.connect(self.add_port) self.ui.addPortButton.clicked.connect(self.add_port)
self.ui.removePortButton.clicked.connect(self.remove_port) self.ui.removePortButton.clicked.connect(self.remove_port)
@@ -49,6 +61,9 @@ class PortOptionsDialog(QDialog):
self.ui.rowsSpin.valueChanged.connect(self._store_current) self.ui.rowsSpin.valueChanged.connect(self._store_current)
self.ui.columnsSpin.valueChanged.connect(self._store_current) self.ui.columnsSpin.valueChanged.connect(self._store_current)
self.ui.descriptionEdit.textChanged.connect(self._store_current) self.ui.descriptionEdit.textChanged.connect(self._store_current)
self.ui.domainCombo.currentIndexChanged.connect(self._power_domain_changed)
self.ui.causalityCombo.currentTextChanged.connect(self._store_current)
self.ui.powerDescriptionEdit.textChanged.connect(self._store_current)
self.ui.portSplitter.setSizes([250, 370]) self.ui.portSplitter.setSizes([250, 370])
if read_only: if read_only:
self.ui.addPortButton.setEnabled(False) self.ui.addPortButton.setEnabled(False)
@@ -64,7 +79,11 @@ class PortOptionsDialog(QDialog):
@property @property
def inputs(self) -> list[Port]: def inputs(self) -> list[Port]:
return [port for port, orientation in self.ports if orientation == "input"] return [
port
for port, orientation in self.ports
if orientation in {"input", "indifferent"}
]
@property @property
def outputs(self) -> list[Port]: def outputs(self) -> list[Port]:
@@ -96,6 +115,9 @@ class PortOptionsDialog(QDialog):
self.ui.rowsSpin.setValue(port.rows) self.ui.rowsSpin.setValue(port.rows)
self.ui.columnsSpin.setValue(port.columns) self.ui.columnsSpin.setValue(port.columns)
self.ui.descriptionEdit.setPlainText(port.description) self.ui.descriptionEdit.setPlainText(port.description)
self.ui.domainCombo.setCurrentIndex(self.ui.domainCombo.findData(port.domain))
self.ui.causalityCombo.setCurrentText(port.causality)
self.ui.powerDescriptionEdit.setPlainText(port.description)
self._show_type_options(port.type) self._show_type_options(port.type)
else: else:
self.ui.nameEdit.clear() self.ui.nameEdit.clear()
@@ -113,15 +135,27 @@ class PortOptionsDialog(QDialog):
self.ui.typeOptionsStack.setEnabled(enabled and not self.read_only) self.ui.typeOptionsStack.setEnabled(enabled and not self.read_only)
def _port_type_changed(self) -> None: def _port_type_changed(self) -> None:
self._show_type_options(self.ui.typeCombo.currentData()) port_type = self.ui.typeCombo.currentData()
indifferent_item = self.ui.orientationCombo.model().item(2)
indifferent_item.setEnabled(port_type == "power")
if port_type != "power" and self.ui.orientationCombo.currentData() == "indifferent":
self.ui.orientationCombo.setCurrentIndex(0)
self._show_type_options(port_type)
self._store_current() self._store_current()
def _show_type_options(self, port_type: str) -> None: def _show_type_options(self, port_type: str) -> None:
self.ui.typeOptionsStack.setCurrentWidget( self.ui.orientationCombo.model().item(2).setEnabled(port_type == "power")
self.ui.signalOptionsPage page = {
if port_type == "signal" "signal": self.ui.signalOptionsPage,
else self.ui.unsupportedTypePage "power": self.ui.powerOptionsPage,
) }.get(port_type, self.ui.unsupportedTypePage)
self.ui.typeOptionsStack.setCurrentWidget(page)
def _power_domain_changed(self) -> None:
domain = power_domain(self.ui.domainCombo.currentData())
self.ui.effortValueLabel.setText(domain.effort)
self.ui.flowValueLabel.setText(domain.flow)
self._store_current()
def _store_current(self) -> None: def _store_current(self) -> None:
row = self.ui.portList.currentRow() row = self.ui.portList.currentRow()
@@ -136,8 +170,16 @@ class PortOptionsDialog(QDialog):
port.unit = self.ui.unitCombo.currentText().strip() port.unit = self.ui.unitCombo.currentText().strip()
port.rows = self.ui.rowsSpin.value() port.rows = self.ui.rowsSpin.value()
port.columns = self.ui.columnsSpin.value() port.columns = self.ui.columnsSpin.value()
port.description = self.ui.descriptionEdit.toPlainText() port.domain = self.ui.domainCombo.currentData() or "power"
self.ports[row] = (port, self.ui.orientationCombo.currentData()) port.causality = self.ui.causalityCombo.currentText()
port.description = (
self.ui.powerDescriptionEdit.toPlainText()
if port.type == "power"
else self.ui.descriptionEdit.toPlainText()
)
orientation = self.ui.orientationCombo.currentData()
port.orientation = orientation
self.ports[row] = (port, orientation)
self.ui.portList.item(row).setText( self.ui.portList.item(row).setText(
f"{port.name} [{self.ports[row][1]}, {port.type}]" f"{port.name} [{self.ports[row][1]}, {port.type}]"
) )
@@ -146,8 +188,8 @@ class PortOptionsDialog(QDialog):
def set_ports(self, inputs: list[Port], outputs: list[Port]) -> None: def set_ports(self, inputs: list[Port], outputs: list[Port]) -> None:
self._loading = True self._loading = True
self.ports = [ self.ports = [
*((deepcopy(port), "input") for port in inputs), *((_oriented_copy(port, "input"), port.orientation or "input") for port in inputs),
*((deepcopy(port), "output") for port in outputs), *((_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)

View File

@@ -92,6 +92,7 @@ class Ui_PortOptionsDialog(object):
self.orientationCombo = QComboBox(self.portDetailsPanel) self.orientationCombo = QComboBox(self.portDetailsPanel)
self.orientationCombo.addItem("") self.orientationCombo.addItem("")
self.orientationCombo.addItem("") self.orientationCombo.addItem("")
self.orientationCombo.addItem("")
self.orientationCombo.setObjectName(u"orientationCombo") self.orientationCombo.setObjectName(u"orientationCombo")
self.portDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.orientationCombo) self.portDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.orientationCombo)
@@ -185,6 +186,61 @@ class Ui_PortOptionsDialog(object):
self.signalOptionsForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.descriptionEdit) self.signalOptionsForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.descriptionEdit)
self.typeOptionsStack.addWidget(self.signalOptionsPage) self.typeOptionsStack.addWidget(self.signalOptionsPage)
self.powerOptionsPage = QWidget()
self.powerOptionsPage.setObjectName(u"powerOptionsPage")
self.powerOptionsForm = QFormLayout(self.powerOptionsPage)
self.powerOptionsForm.setObjectName(u"powerOptionsForm")
self.domainLabel = QLabel(self.powerOptionsPage)
self.domainLabel.setObjectName(u"domainLabel")
self.powerOptionsForm.setWidget(0, QFormLayout.ItemRole.LabelRole, self.domainLabel)
self.domainCombo = QComboBox(self.powerOptionsPage)
self.domainCombo.setObjectName(u"domainCombo")
self.powerOptionsForm.setWidget(0, QFormLayout.ItemRole.FieldRole, self.domainCombo)
self.effortLabel = QLabel(self.powerOptionsPage)
self.effortLabel.setObjectName(u"effortLabel")
self.powerOptionsForm.setWidget(1, QFormLayout.ItemRole.LabelRole, self.effortLabel)
self.effortValueLabel = QLabel(self.powerOptionsPage)
self.effortValueLabel.setObjectName(u"effortValueLabel")
self.powerOptionsForm.setWidget(1, QFormLayout.ItemRole.FieldRole, self.effortValueLabel)
self.flowLabel = QLabel(self.powerOptionsPage)
self.flowLabel.setObjectName(u"flowLabel")
self.powerOptionsForm.setWidget(2, QFormLayout.ItemRole.LabelRole, self.flowLabel)
self.flowValueLabel = QLabel(self.powerOptionsPage)
self.flowValueLabel.setObjectName(u"flowValueLabel")
self.powerOptionsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.flowValueLabel)
self.causalityLabel = QLabel(self.powerOptionsPage)
self.causalityLabel.setObjectName(u"causalityLabel")
self.powerOptionsForm.setWidget(3, QFormLayout.ItemRole.LabelRole, self.causalityLabel)
self.causalityCombo = QComboBox(self.powerOptionsPage)
self.causalityCombo.setObjectName(u"causalityCombo")
self.powerOptionsForm.setWidget(3, QFormLayout.ItemRole.FieldRole, self.causalityCombo)
self.powerDescriptionLabel = QLabel(self.powerOptionsPage)
self.powerDescriptionLabel.setObjectName(u"powerDescriptionLabel")
self.powerOptionsForm.setWidget(4, QFormLayout.ItemRole.LabelRole, self.powerDescriptionLabel)
self.powerDescriptionEdit = QPlainTextEdit(self.powerOptionsPage)
self.powerDescriptionEdit.setObjectName(u"powerDescriptionEdit")
self.powerOptionsForm.setWidget(4, QFormLayout.ItemRole.FieldRole, self.powerDescriptionEdit)
self.typeOptionsStack.addWidget(self.powerOptionsPage)
self.unsupportedTypePage = QWidget() self.unsupportedTypePage = QWidget()
self.unsupportedTypePage.setObjectName(u"unsupportedTypePage") self.unsupportedTypePage.setObjectName(u"unsupportedTypePage")
self.unsupportedTypeLayout = QVBoxLayout(self.unsupportedTypePage) self.unsupportedTypeLayout = QVBoxLayout(self.unsupportedTypePage)
@@ -228,6 +284,7 @@ class Ui_PortOptionsDialog(object):
self.orientationLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Orientation:", None)) self.orientationLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Orientation:", None))
self.orientationCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Input", None)) self.orientationCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Input", None))
self.orientationCombo.setItemText(1, QCoreApplication.translate("PortOptionsDialog", u"Output", None)) self.orientationCombo.setItemText(1, QCoreApplication.translate("PortOptionsDialog", u"Output", None))
self.orientationCombo.setItemText(2, QCoreApplication.translate("PortOptionsDialog", u"Indifferent", None))
self.multipleConnectionsCheckBox.setText(QCoreApplication.translate("PortOptionsDialog", u"Allow multiple connections", None)) self.multipleConnectionsCheckBox.setText(QCoreApplication.translate("PortOptionsDialog", u"Allow multiple connections", None))
self.positionHintLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"New ports start at (0, 0) in the icon editor.", None)) self.positionHintLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"New ports start at (0, 0) in the icon editor.", None))
@@ -241,6 +298,13 @@ class Ui_PortOptionsDialog(object):
self.unitLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Unit:", None)) self.unitLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Unit:", None))
self.rowsColumnsLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Matrix size:", None)) self.rowsColumnsLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Matrix size:", None))
self.descriptionLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Description:", None)) self.descriptionLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Description:", None))
self.domainLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Domain:", None))
self.effortLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Effort:", None))
self.effortValueLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"p.e", None))
self.flowLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Flow:", None))
self.flowValueLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"p.f", None))
self.causalityLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Causality:", None))
self.powerDescriptionLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"Description:", None))
self.unsupportedTypeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"This port type does not have an options editor yet.", None)) self.unsupportedTypeLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"This port type does not have an options editor yet.", None))
# retranslateUi # retranslateUi

View File

@@ -1,8 +1,12 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Literal
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
ArrowStyle = Literal["open", "half", "filled"]
@dataclass(frozen=True) @dataclass(frozen=True)
class ConnectionStyle: class ConnectionStyle:
color: str = "#285f9e" color: str = "#285f9e"
@@ -13,11 +17,17 @@ class ConnectionStyle:
arrow_at_source: bool = False arrow_at_source: bool = False
arrow_at_target: bool = True arrow_at_target: bool = True
arrow_size: float = 10.0 arrow_size: float = 10.0
arrow_style: ArrowStyle = "filled"
def __post_init__(self) -> None:
if self.arrow_style not in {"open", "half", "filled"}:
raise ValueError(f"Unknown connection arrow style: {self.arrow_style}")
# This is the intentional code-level styling point for every port/connection type. # This is the intentional code-level styling point for every port/connection type.
CONNECTION_STYLES: dict[str, ConnectionStyle] = { CONNECTION_STYLES: dict[str, ConnectionStyle] = {
"signal": ConnectionStyle(), "signal": ConnectionStyle(),
"power": ConnectionStyle(width=3.0, arrow_style="half", color="#000000"),
} }

View File

@@ -1,6 +1,6 @@
from copy import deepcopy from copy import deepcopy
from PySide6.QtCore import QPointF, QRectF, QSizeF, Qt from PySide6.QtCore import QPointF, QRectF, QSizeF, Qt, QTimer
from PySide6.QtGui import QColor, QPainter, QPainterPath, QPen, QPolygonF from PySide6.QtGui import QColor, QPainter, QPainterPath, QPen, QPolygonF
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QColorDialog, QColorDialog,
@@ -487,7 +487,13 @@ class IconEditorDialog(QDialog):
self.scene.addItem(ShapeItem(element)) self.scene.addItem(ShapeItem(element))
self._add_ports(self.inputs, "input", 0.0) self._add_ports(self.inputs, "input", 0.0)
self._add_ports(self.outputs, "output", self.icon.width) self._add_ports(self.outputs, "output", self.icon.width)
self.view.center_icon() self._initial_fit_pending = True
def showEvent(self, event) -> None: # noqa: N802 (Qt API name)
super().showEvent(event)
if self._initial_fit_pending:
self._initial_fit_pending = False
QTimer.singleShot(0, self.view.center_icon)
def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None: def _add_ports(self, ports: list[Port], direction: str, default_x: float) -> None:
spacing = self.icon.height / (len(ports) + 1) spacing = self.icon.height / (len(ports) + 1)

View File

@@ -485,23 +485,39 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
handle.setVisible(self.isSelected()) handle.setVisible(self.isSelected())
@staticmethod @staticmethod
def _arrow(end: QPointF, direction: QPointF, size: float) -> QPolygonF: def _arrow_points(
end: QPointF, direction: QPointF, size: float
) -> tuple[QPointF, QPointF, QPointF]:
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5) length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5)
unit = QPointF(direction.x() / length, direction.y() / length) unit = QPointF(direction.x() / length, direction.y() / length)
normal = QPointF(-unit.y(), unit.x()) normal = QPointF(-unit.y(), unit.x())
base = end - unit * size base = end - unit * size
return QPolygonF([end, base + normal * size * 0.45, base - normal * size * 0.45]) return end, base + normal * size * 0.45, base - normal * size * 0.45
def _draw_arrow(
self, painter: QPainter, end: QPointF, direction: QPointF
) -> None:
tip, left, right = self._arrow_points(
end, direction, self.style.arrow_size
)
color = self.pen().color()
if self.style.arrow_style == "filled":
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(color)
painter.drawPolygon(QPolygonF([tip, left, right]))
return
painter.setPen(QPen(color, self.pen().widthF(), Qt.PenStyle.SolidLine))
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawLine(tip, left)
if self.style.arrow_style == "open":
painter.drawLine(tip, right)
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)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(self.pen().color())
if self.style.arrow_at_target and not self.target_is_junction: if self.style.arrow_at_target and not self.target_is_junction:
painter.drawPolygon(self._arrow(self.end, self.end_direction, self.style.arrow_size)) 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:
painter.drawPolygon( self._draw_arrow(painter, self.start, -self.start_direction)
self._arrow(self.start, -self.start_direction, self.style.arrow_size)
)
class WaypointHandle(QGraphicsEllipseItem): class WaypointHandle(QGraphicsEllipseItem):
@@ -1207,9 +1223,22 @@ class GraphScene(QGraphicsScene):
def add_pairs( def add_pairs(
source_item: ComponentGraphicsItem, target_item: ComponentGraphicsItem source_item: ComponentGraphicsItem, target_item: ComponentGraphicsItem
) -> None: ) -> None:
for output in source_item.component.outputs: source_ports = [
*source_item.component.outputs,
*(
port
for port in source_item.component.inputs
if port.orientation == "indifferent"
),
]
for output in source_ports:
for input_port in target_item.component.inputs: for input_port in target_item.component.inputs:
if not PortTypeRegistry.compatible(output.type, input_port.type): if not PortTypeRegistry.compatible(
output.type,
input_port.type,
output.domain,
input_port.domain,
):
continue continue
source = Endpoint(block=source_item.component_id, port=output.id) source = Endpoint(block=source_item.component_id, port=output.id)
target = Endpoint(block=target_item.component_id, port=input_port.id) target = Endpoint(block=target_item.component_id, port=input_port.id)
@@ -1240,7 +1269,9 @@ class GraphScene(QGraphicsScene):
if terminal.direction == "input": if terminal.direction == "input":
for port in component.component.inputs: for port in component.component.inputs:
target = Endpoint(block=component.component_id, port=port.id) target = Endpoint(block=component.component_id, port=port.id)
if not PortTypeRegistry.compatible(terminal.port.type, port.type): if not PortTypeRegistry.compatible(
terminal.port.type, port.type, terminal.port.domain, port.domain
):
continue continue
if not self.controller.endpoint_accepts_connection(interface, "source"): if not self.controller.endpoint_accepts_connection(interface, "source"):
continue continue
@@ -1254,9 +1285,19 @@ class GraphScene(QGraphicsScene):
) )
) )
else: else:
for port in component.component.outputs: ports = [
*component.component.outputs,
*(
port
for port in component.component.inputs
if port.orientation == "indifferent"
),
]
for port in ports:
source = Endpoint(block=component.component_id, port=port.id) source = Endpoint(block=component.component_id, port=port.id)
if not PortTypeRegistry.compatible(port.type, terminal.port.type): if not PortTypeRegistry.compatible(
port.type, terminal.port.type, port.domain, terminal.port.domain
):
continue continue
if not self.controller.endpoint_accepts_connection(source, "source"): if not self.controller.endpoint_accepts_connection(source, "source"):
continue continue

View File

@@ -94,6 +94,11 @@
<string>Output</string> <string>Output</string>
</property> </property>
</item> </item>
<item>
<property name="text">
<string>Indifferent</string>
</property>
</item>
</widget> </widget>
</item> </item>
<item row="3" column="0" colspan="2"> <item row="3" column="0" colspan="2">
@@ -222,6 +227,20 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="powerOptionsPage">
<layout class="QFormLayout" name="powerOptionsForm">
<item row="0" column="0"><widget class="QLabel" name="domainLabel"><property name="text"><string>Domain:</string></property></widget></item>
<item row="0" column="1"><widget class="QComboBox" name="domainCombo"/></item>
<item row="1" column="0"><widget class="QLabel" name="effortLabel"><property name="text"><string>Effort:</string></property></widget></item>
<item row="1" column="1"><widget class="QLabel" name="effortValueLabel"><property name="text"><string>p.e</string></property></widget></item>
<item row="2" column="0"><widget class="QLabel" name="flowLabel"><property name="text"><string>Flow:</string></property></widget></item>
<item row="2" column="1"><widget class="QLabel" name="flowValueLabel"><property name="text"><string>p.f</string></property></widget></item>
<item row="3" column="0"><widget class="QLabel" name="causalityLabel"><property name="text"><string>Causality:</string></property></widget></item>
<item row="3" column="1"><widget class="QComboBox" name="causalityCombo"/></item>
<item row="4" column="0"><widget class="QLabel" name="powerDescriptionLabel"><property name="text"><string>Description:</string></property></widget></item>
<item row="4" column="1"><widget class="QPlainTextEdit" name="powerDescriptionEdit"/></item>
</layout>
</widget>
<widget class="QWidget" name="unsupportedTypePage"> <widget class="QWidget" name="unsupportedTypePage">
<layout class="QVBoxLayout" name="unsupportedTypeLayout"> <layout class="QVBoxLayout" name="unsupportedTypeLayout">
<item> <item>

View File

@@ -6,7 +6,7 @@
}, },
"roots": [ "roots": [
{ {
"id": "4014ba90-5538-4050-a9be-552318197ce1", "id": "4aefc7e3-ac23-4b31-9087-bbad16156be0",
"name": "New Graph Block 1", "name": "New Graph Block 1",
"position": { "position": {
"x": 0.0, "x": 0.0,
@@ -61,142 +61,18 @@
"graph": { "graph": {
"blocks": [ "blocks": [
{ {
"id": "86ba2bf6-0da5-40a4-98e9-fb0d9c12c576", "id": "c6169e52-e993-4293-8fc8-5b97e8ad59be",
"name": "Constant0", "name": "C",
"position": {
"x": -224.0,
"y": -160.0
},
"rotation": 0.0,
"interface": {
"inputs": [],
"outputs": [
{
"id": "port-ddc97277",
"name": "y",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 88.0,
"y": 40.0
}
},
"type": "signal",
"multipleConnections": false,
"valueType": "real",
"quantity": "Length",
"unit": "m",
"dimensions": {
"rows": 1,
"columns": 1
},
"description": ""
}
]
},
"parameters": [
{
"id": "parameter-022177fc",
"name": "v",
"type": "real",
"value": "2",
"quantity": "",
"unit": "",
"dimensions": {
"rows": 1,
"columns": 1
},
"description": ""
}
],
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"cornerRadius": 5.0,
"fill": "#f4f4f4",
"height": 64.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"type": "rectangle",
"width": 64.0,
"x": 32.0,
"y": 32.0
},
{
"fill": "none",
"height": 48.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"type": "line",
"width": 0.0,
"x": 40.0,
"y": 40.0
},
{
"fill": "none",
"height": 0.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"type": "line",
"width": 48.0,
"x": 40.0,
"y": 88.0
},
{
"fill": "none",
"height": 0.0,
"lineStyle": "solid",
"lineWidth": 1.0,
"stroke": "#ffaa00",
"type": "line",
"width": 48.0,
"x": 40.0,
"y": 64.0
}
]
},
"properties": {
"showName": true
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "y = v;",
"declarations": "",
"initialEquations": ""
}
}
},
{
"id": "7805cb2f-c938-44b2-a1f8-e1b4af13f7bf",
"name": "Gain0",
"position": { "position": {
"x": -96.0, "x": -96.0,
"y": -160.0 "y": -288.0
}, },
"rotation": 0.0, "rotation": 0.0,
"interface": { "interface": {
"inputs": [ "inputs": [
{ {
"id": "port-df34ce84", "id": "port-2e1d884f",
"name": "u", "name": "p",
"position": { "position": {
"x": 0.0, "x": 0.0,
"y": 0.0 "y": 0.0
@@ -207,7 +83,7 @@
"y": 64.0 "y": 64.0
} }
}, },
"type": "signal", "type": "power",
"multipleConnections": false, "multipleConnections": false,
"valueType": "real", "valueType": "real",
"quantity": "", "quantity": "",
@@ -216,13 +92,16 @@
"rows": 1, "rows": 1,
"columns": 1 "columns": 1
}, },
"description": "" "description": "",
"orientation": "input",
"domain": "power",
"causality": "preferred effort out"
} }
], ],
"outputs": [ "outputs": [
{ {
"id": "port-fe9e6486", "id": "port-ca7716d5",
"name": "y", "name": "state",
"position": { "position": {
"x": 0.0, "x": 0.0,
"y": 0.0 "y": 0.0
@@ -242,18 +121,21 @@
"rows": 1, "rows": 1,
"columns": 1 "columns": 1
}, },
"description": "" "description": "",
"orientation": "output",
"domain": "power",
"causality": "indifferent"
} }
] ]
}, },
"parameters": [ "parameters": [
{ {
"id": "parameter-61a43a86", "id": "parameter-d056bc27",
"name": "k", "name": "c",
"type": "real", "type": "real",
"value": "2.5", "value": "1",
"quantity": "Current", "quantity": "",
"unit": "A", "unit": "",
"dimensions": { "dimensions": {
"rows": 1, "rows": 1,
"columns": 1 "columns": 1
@@ -272,35 +154,23 @@
}, },
"elements": [ "elements": [
{ {
"cornerRadius": 5.0, "type": "text",
"fill": "#f4f4f4", "x": 40.0,
"height": 64.0, "y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "C",
"color": "#303030",
"fontSize": 32.0,
"lineStyle": "solid", "lineStyle": "solid",
"lineWidth": 1.5, "lineWidth": 1.5,
"stroke": "#303030", "stroke": "#303030",
"type": "rectangle", "fill": "#ffffff"
"width": 64.0,
"x": 32.0,
"y": 32.0
},
{
"color": "#00007f",
"fill": "#ffffff",
"fontSize": 24.0,
"height": 48.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"text": "K",
"type": "text",
"width": 48.0,
"x": 40.0,
"y": 40.0
} }
] ]
}, },
"properties": { "properties": {
"showName": true "showName": false
}, },
"library": { "library": {
"showSubtree": true "showSubtree": true
@@ -308,23 +178,327 @@
"implementation": { "implementation": {
"kind": "text", "kind": "text",
"source": { "source": {
"equations": "y = k*u;", "equations": "der(state) = p.f;\np.e = state/c;",
"declarations": "", "declarations": "",
"initialEquations": "" "initialEquations": ""
} }
} }
},
{
"id": "bfa506d8-ee41-4bab-ae19-a97fd67bfcf6",
"name": "1",
"position": {
"x": -96.0,
"y": -160.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-3e53bf80",
"name": "p",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "power",
"multipleConnections": true,
"valueType": "real",
"quantity": "",
"unit": "",
"dimensions": {
"rows": 1,
"columns": 1
},
"description": "",
"orientation": "indifferent",
"domain": "power",
"causality": "indifferent"
}
],
"outputs": []
},
"parameters": [],
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "1",
"color": "#303030",
"fontSize": 32.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"fill": "#ffffff"
}
]
},
"properties": {
"showName": false
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "flow = p[1].f;\nsum(p[i].e for i in 1:$p_N$) = 0;\nfor i in 2:$p_N$ loop\n p[i].f = p[i-1].f;\nend for;",
"declarations": "Real flow;",
"initialEquations": ""
}
}
},
{
"id": "ea95e8ee-7f2d-4e14-a65d-d56bec5b4eb2",
"name": "R",
"position": {
"x": 32.0,
"y": -160.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-2e1d884f",
"name": "p",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "power",
"multipleConnections": false,
"valueType": "real",
"quantity": "",
"unit": "",
"dimensions": {
"rows": 1,
"columns": 1
},
"description": "",
"orientation": "input",
"domain": "power",
"causality": "indifferent"
}
],
"outputs": []
},
"parameters": [
{
"id": "parameter-d056bc27",
"name": "r",
"type": "real",
"value": "1",
"quantity": "",
"unit": "",
"dimensions": {
"rows": 1,
"columns": 1
},
"description": ""
}
],
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "R",
"color": "#303030",
"fontSize": 32.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"fill": "#ffffff"
}
]
},
"properties": {
"showName": false
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "p.e = r*p.f;",
"declarations": "",
"initialEquations": ""
}
}
},
{
"id": "063a6341-cb18-4bfe-a1e4-00957a88a874",
"name": "Se",
"position": {
"x": -224.0,
"y": -160.0
},
"rotation": 0.0,
"interface": {
"inputs": [],
"outputs": [
{
"id": "port-2e1d884f",
"name": "p",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "power",
"multipleConnections": false,
"valueType": "real",
"quantity": "",
"unit": "",
"dimensions": {
"rows": 1,
"columns": 1
},
"description": "",
"orientation": "output",
"domain": "power",
"causality": "fixed effort out"
}
]
},
"parameters": [
{
"id": "parameter-d056bc27",
"name": "effort",
"type": "real",
"value": "1",
"quantity": "",
"unit": "",
"dimensions": {
"rows": 1,
"columns": 1
},
"description": ""
}
],
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "text",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"text": "Se",
"color": "#303030",
"fontSize": 32.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"fill": "#ffffff"
}
]
},
"properties": {
"showName": false
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "p.e = effort;\nflow = p.f;",
"declarations": "Real flow;",
"initialEquations": ""
}
}
} }
], ],
"connections": [ "connections": [
{ {
"id": "5582d4ad-7c7a-40fe-87d2-9b78410336b3", "id": "4b09ba21-baa0-4db7-9431-4c919478bce4",
"source": { "source": {
"block": "86ba2bf6-0da5-40a4-98e9-fb0d9c12c576", "block": "063a6341-cb18-4bfe-a1e4-00957a88a874",
"port": "port-ddc97277" "port": "port-2e1d884f"
}, },
"target": { "target": {
"block": "7805cb2f-c938-44b2-a1f8-e1b4af13f7bf", "block": "bfa506d8-ee41-4bab-ae19-a97fd67bfcf6",
"port": "port-df34ce84" "port": "port-3e53bf80"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "a73a99a1-54e2-461d-a9a7-2d6d11d500f4",
"source": {
"block": "bfa506d8-ee41-4bab-ae19-a97fd67bfcf6",
"port": "port-3e53bf80"
},
"target": {
"block": "ea95e8ee-7f2d-4e14-a65d-d56bec5b4eb2",
"port": "port-2e1d884f"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "dd5fb1e2-b125-4b5f-a792-dac89dd89ad3",
"source": {
"block": "bfa506d8-ee41-4bab-ae19-a97fd67bfcf6",
"port": "port-3e53bf80"
},
"target": {
"block": "c6169e52-e993-4293-8fc8-5b97e8ad59be",
"port": "port-2e1d884f"
}, },
"name": "", "name": "",
"properties": { "properties": {