Compare commits
3 Commits
8f87a3b477
...
495edd42f4
| Author | SHA1 | Date | |
|---|---|---|---|
| 495edd42f4 | |||
| a067c85994 | |||
| bf2512995f |
@@ -60,6 +60,9 @@ 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
|
||||
existing connection.
|
||||
- Connections reference port IDs, never port names.
|
||||
- Connection junctions are explicit typed graph objects. Splitting a connection
|
||||
creates one incoming and one outgoing segment; the junction can source further
|
||||
branches without overlapping full connection paths.
|
||||
- Graph interaction has separate Pointer and Connect modes. Port hints are only
|
||||
visible in Connect mode. Connecting two blocks opens the compatible port-pair
|
||||
chooser; explicit port clicks determine its default selection. Connections
|
||||
@@ -121,6 +124,9 @@ pyside6-uic --from-imports ui/shape_options_dialog.ui \
|
||||
|
||||
pyside6-uic --from-imports ui/icon_editor_dialog.ui \
|
||||
-o src/bedit/gui/generated/ui_icon_editor_dialog.py
|
||||
|
||||
pyside6-uic --from-imports ui/text_definition_editor.ui \
|
||||
-o src/bedit/gui/generated/ui_text_definition_editor.py
|
||||
```
|
||||
|
||||
When adding a promoted/custom widget in Designer, its header must use the real
|
||||
|
||||
@@ -13,7 +13,7 @@ class LibraryDocument:
|
||||
|
||||
|
||||
def bundled_library_path() -> Path:
|
||||
return Path(__file__).resolve().parents[1] / "data" / "libraries" / "example.json"
|
||||
return Path(__file__).resolve().parents[1] / "data" / "libraries" / "default.json"
|
||||
|
||||
|
||||
def default_library_paths() -> list[str]:
|
||||
|
||||
@@ -16,6 +16,7 @@ class Port:
|
||||
y: float = 0.0
|
||||
properties: dict[str, Any] = field(default_factory=dict)
|
||||
type: str = "signal"
|
||||
allows_multiple_connections: bool = False
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
@@ -24,6 +25,7 @@ class Port:
|
||||
"position": {"x": self.x, "y": self.y},
|
||||
"properties": self.properties,
|
||||
"type": self.type,
|
||||
"multipleConnections": self.allows_multiple_connections,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -36,6 +38,7 @@ class Port:
|
||||
y=float(position.get("y", 0.0)),
|
||||
properties=dict(data.get("properties", {})),
|
||||
type=str(data.get("type", "signal")),
|
||||
allows_multiple_connections=bool(data.get("multipleConnections", False)),
|
||||
)
|
||||
|
||||
|
||||
@@ -109,10 +112,13 @@ class Endpoint:
|
||||
block: str | None = None
|
||||
port: str | None = None
|
||||
interface: str | None = None
|
||||
junction: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, str]:
|
||||
if self.interface is not None:
|
||||
return {"interface": self.interface}
|
||||
if self.junction is not None:
|
||||
return {"junction": self.junction}
|
||||
if self.block is None or self.port is None:
|
||||
raise ValueError("A block endpoint requires both block and port")
|
||||
return {"block": self.block, "port": self.port}
|
||||
@@ -121,6 +127,8 @@ class Endpoint:
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Endpoint":
|
||||
if "interface" in data:
|
||||
return cls(interface=str(data["interface"]))
|
||||
if "junction" in data:
|
||||
return cls(junction=str(data["junction"]))
|
||||
return cls(block=str(data["block"]), port=str(data["port"]))
|
||||
|
||||
|
||||
@@ -152,6 +160,55 @@ class Connection:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Parameter:
|
||||
id: str
|
||||
name: str
|
||||
type: str = "real"
|
||||
value: str = "0"
|
||||
|
||||
def to_dict(self) -> dict[str, str]:
|
||||
return {"id": self.id, "name": self.name, "type": self.type, "value": self.value}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Parameter":
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("Each text component parameter must be an object")
|
||||
if "id" not in data:
|
||||
raise ValueError("Each text component parameter must have an ID")
|
||||
return cls(
|
||||
id=str(data["id"]),
|
||||
name=str(data.get("name", "")),
|
||||
type=str(data.get("type", "real")),
|
||||
value=str(data.get("value", "0")),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Junction:
|
||||
id: str
|
||||
x: float
|
||||
y: float
|
||||
type: str = "signal"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"position": {"x": self.x, "y": self.y},
|
||||
"type": self.type,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Junction":
|
||||
position = data.get("position", {})
|
||||
return cls(
|
||||
id=str(data["id"]),
|
||||
x=float(position.get("x", 0)),
|
||||
y=float(position.get("y", 0)),
|
||||
type=str(data.get("type", "signal")),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Annotation:
|
||||
id: str
|
||||
@@ -197,12 +254,14 @@ class Graph:
|
||||
blocks: dict[str, Component] = field(default_factory=dict)
|
||||
connections: dict[str, Connection] = field(default_factory=dict)
|
||||
annotations: dict[str, Annotation] = field(default_factory=dict)
|
||||
junctions: dict[str, Junction] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"blocks": [block.to_dict() for block in self.blocks.values()],
|
||||
"connections": [connection.to_dict() for connection in self.connections.values()],
|
||||
"annotations": [item.to_dict() for item in self.annotations.values()],
|
||||
"junctions": [junction.to_dict() for junction in self.junctions.values()],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -211,18 +270,22 @@ class Graph:
|
||||
blocks = [Component.from_dict(item) for item in data.get("blocks", [])]
|
||||
connections = [Connection.from_dict(item) for item in data.get("connections", [])]
|
||||
annotations = [Annotation.from_dict(item) for item in data.get("annotations", [])]
|
||||
junctions = [Junction.from_dict(item) for item in data.get("junctions", [])]
|
||||
if len({block.id for block in blocks}) != len(blocks):
|
||||
raise ValueError("A graph contains duplicate component IDs")
|
||||
if len({connection.id for connection in connections}) != len(connections):
|
||||
raise ValueError("A graph contains duplicate connection IDs")
|
||||
if len({item.id for item in annotations}) != len(annotations):
|
||||
raise ValueError("A graph contains duplicate annotation IDs")
|
||||
if len({item.id for item in junctions}) != len(junctions):
|
||||
raise ValueError("A graph contains duplicate junction IDs")
|
||||
if any(item.kind not in {"box", "line", "text"} for item in annotations):
|
||||
raise ValueError("A graph contains an unknown annotation kind")
|
||||
return cls(
|
||||
blocks={block.id: block for block in blocks},
|
||||
connections={connection.id: connection for connection in connections},
|
||||
annotations={item.id: item for item in annotations},
|
||||
junctions={junction.id: junction for junction in junctions},
|
||||
)
|
||||
|
||||
|
||||
@@ -271,6 +334,19 @@ class Component:
|
||||
kind = str(implementation.get("kind", "graph"))
|
||||
if kind not in {"graph", "text"}:
|
||||
raise ValueError(f"Unknown component implementation kind: {kind}")
|
||||
source: dict[str, Any] = {}
|
||||
if kind == "text":
|
||||
raw_source = implementation.get("source", {})
|
||||
equations = raw_source.get("equations", "")
|
||||
parameters = raw_source.get("parameters", [])
|
||||
if not isinstance(equations, str):
|
||||
raise ValueError("Text component equations must be a string")
|
||||
if not isinstance(parameters, list):
|
||||
raise ValueError("Text component parameters must be a list")
|
||||
source = {
|
||||
"equations": equations,
|
||||
"parameters": [Parameter.from_dict(item).to_dict() for item in parameters],
|
||||
}
|
||||
return cls(
|
||||
id=str(data["id"]),
|
||||
name=str(data.get("name", "Unnamed")),
|
||||
@@ -284,7 +360,7 @@ class Component:
|
||||
show_subtree_in_library=bool(data.get("library", {}).get("showSubtree", True)),
|
||||
implementation_kind=kind,
|
||||
graph=Graph.from_dict(implementation.get("graph")) if kind == "graph" else Graph(),
|
||||
source=dict(implementation.get("source", {})) if kind == "text" else {},
|
||||
source=source,
|
||||
)
|
||||
|
||||
|
||||
@@ -295,7 +371,7 @@ class GraphDocument:
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> "GraphDocument":
|
||||
return cls(metadata={"name": "Untitled"})
|
||||
return cls(metadata={"name": "Current Document"})
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
@@ -348,6 +424,9 @@ class GraphDocument:
|
||||
|
||||
def validate(self) -> None:
|
||||
seen: set[str] = set()
|
||||
root_names = [component.name for component in self.roots.values()]
|
||||
if len(set(root_names)) != len(root_names):
|
||||
raise ValueError("Root component names must be unique")
|
||||
for component in self.all_components():
|
||||
if component.id in seen:
|
||||
raise ValueError(f"Duplicate component ID: {component.id}")
|
||||
@@ -358,14 +437,29 @@ class GraphDocument:
|
||||
|
||||
@staticmethod
|
||||
def _validate_graph(owner: Component) -> None:
|
||||
child_names = [component.name for component in owner.graph.blocks.values()]
|
||||
if len(set(child_names)) != len(child_names):
|
||||
raise ValueError(
|
||||
f"Component names inside {owner.name!r} must be unique"
|
||||
)
|
||||
input_ids = {port.id for port in owner.inputs}
|
||||
output_ids = {port.id for port in owner.outputs}
|
||||
if len(input_ids) != len(owner.inputs) or len(output_ids) != len(owner.outputs):
|
||||
raise ValueError(f"Component {owner.name} contains duplicate port IDs")
|
||||
for port in (*owner.inputs, *owner.outputs):
|
||||
PortTypeRegistry.get(port.type)
|
||||
for junction in owner.graph.junctions.values():
|
||||
PortTypeRegistry.get(junction.type)
|
||||
endpoint_counts: dict[tuple[str, str, str], int] = {}
|
||||
for connection in owner.graph.connections.values():
|
||||
if connection.source.interface is not None:
|
||||
if connection.source.junction is not None:
|
||||
junction = owner.graph.junctions.get(connection.source.junction)
|
||||
if junction is None:
|
||||
raise ValueError(
|
||||
f"Connection {connection.id} uses an unknown source junction"
|
||||
)
|
||||
source_port = Port(junction.id, "Junction", type=junction.type)
|
||||
elif connection.source.interface is not None:
|
||||
if connection.source.interface not in input_ids:
|
||||
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)
|
||||
@@ -374,7 +468,19 @@ class GraphDocument:
|
||||
if source is None or connection.source.port not in {p.id for p in source.outputs}:
|
||||
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)
|
||||
if connection.target.interface is not None:
|
||||
if connection.target.junction is not None:
|
||||
junction = owner.graph.junctions.get(connection.target.junction)
|
||||
if junction is None:
|
||||
raise ValueError(
|
||||
f"Connection {connection.id} uses an unknown target junction"
|
||||
)
|
||||
target_port = Port(
|
||||
junction.id,
|
||||
"Junction",
|
||||
type=junction.type,
|
||||
allows_multiple_connections=False,
|
||||
)
|
||||
elif connection.target.interface is not None:
|
||||
if connection.target.interface not in output_ids:
|
||||
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)
|
||||
@@ -385,6 +491,40 @@ class GraphDocument:
|
||||
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):
|
||||
raise ValueError(f"Connection {connection.id} joins incompatible port types")
|
||||
source_key = (
|
||||
"source-junction"
|
||||
if connection.source.junction is not None
|
||||
else "source-interface"
|
||||
if connection.source.interface is not None
|
||||
else "source-block",
|
||||
connection.source.block or "",
|
||||
connection.source.junction
|
||||
or connection.source.interface
|
||||
or connection.source.port
|
||||
or "",
|
||||
)
|
||||
target_key = (
|
||||
"target-junction"
|
||||
if connection.target.junction is not None
|
||||
else "target-interface"
|
||||
if connection.target.interface is not None
|
||||
else "target-block",
|
||||
connection.target.block or "",
|
||||
connection.target.junction
|
||||
or connection.target.interface
|
||||
or connection.target.port
|
||||
or "",
|
||||
)
|
||||
endpoint_counts[source_key] = endpoint_counts.get(source_key, 0) + 1
|
||||
endpoint_counts[target_key] = endpoint_counts.get(target_key, 0) + 1
|
||||
if (
|
||||
endpoint_counts[target_key] > 1
|
||||
and not target_port.allows_multiple_connections
|
||||
):
|
||||
raise ValueError(
|
||||
f"Input {target_port.name!r} has multiple incoming connections "
|
||||
"but does not allow them"
|
||||
)
|
||||
|
||||
|
||||
def clone_component(source: Component) -> Component:
|
||||
@@ -393,10 +533,15 @@ def clone_component(source: Component) -> Component:
|
||||
def clone(current: Component) -> Component:
|
||||
child_pairs = [(child, clone(child)) for child in current.graph.blocks.values()]
|
||||
child_ids = {old.id: new.id for old, new in child_pairs}
|
||||
junction_ids = {
|
||||
junction.id: str(uuid4()) for junction in current.graph.junctions.values()
|
||||
}
|
||||
|
||||
def remap(endpoint: Endpoint) -> Endpoint:
|
||||
if endpoint.interface is not None:
|
||||
return endpoint
|
||||
if endpoint.junction is not None:
|
||||
return Endpoint(junction=junction_ids[endpoint.junction])
|
||||
return Endpoint(block=child_ids[endpoint.block or ""], port=endpoint.port)
|
||||
|
||||
graph = Graph(
|
||||
@@ -427,6 +572,12 @@ def clone_component(source: Component) -> Component:
|
||||
for annotation in current.graph.annotations.values()
|
||||
for new_id in [str(uuid4())]
|
||||
},
|
||||
junctions={
|
||||
junction_ids[junction.id]: Junction(
|
||||
junction_ids[junction.id], junction.x, junction.y, junction.type
|
||||
)
|
||||
for junction in current.graph.junctions.values()
|
||||
},
|
||||
)
|
||||
return Component(
|
||||
id=str(uuid4()),
|
||||
@@ -434,11 +585,27 @@ def clone_component(source: Component) -> Component:
|
||||
x=current.x,
|
||||
y=current.y,
|
||||
inputs=[
|
||||
Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type)
|
||||
Port(
|
||||
port.id,
|
||||
port.name,
|
||||
port.x,
|
||||
port.y,
|
||||
deepcopy(port.properties),
|
||||
port.type,
|
||||
port.allows_multiple_connections,
|
||||
)
|
||||
for port in current.inputs
|
||||
],
|
||||
outputs=[
|
||||
Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type)
|
||||
Port(
|
||||
port.id,
|
||||
port.name,
|
||||
port.x,
|
||||
port.y,
|
||||
deepcopy(port.properties),
|
||||
port.type,
|
||||
port.allows_multiple_connections,
|
||||
)
|
||||
for port in current.outputs
|
||||
],
|
||||
icon=Icon.from_dict(current.icon.to_dict()),
|
||||
|
||||
435
BEdit/src/bedit/data/libraries/default.json
Normal file
435
BEdit/src/bedit/data/libraries/default.json
Normal file
@@ -0,0 +1,435 @@
|
||||
{
|
||||
"format": "bedit-document",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "Untitled"
|
||||
},
|
||||
"roots": [
|
||||
{
|
||||
"id": "7ad651ef-6f76-4064-81d3-3fd6dd4d2918",
|
||||
"name": "gain",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-df34ce84",
|
||||
"name": "in",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 64.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-fe9e6486",
|
||||
"name": "out",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 88.0,
|
||||
"y": 40.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "Text",
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 128.0
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"x": 32.0,
|
||||
"y": 32.0,
|
||||
"width": 64.0,
|
||||
"height": 64.0,
|
||||
"fill": "#f4f4f4",
|
||||
"stroke": "#303030",
|
||||
"lineWidth": 1.5,
|
||||
"lineStyle": "solid",
|
||||
"cornerRadius": 5.0
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 40.0,
|
||||
"y": 40.0,
|
||||
"width": 48.0,
|
||||
"height": 48.0,
|
||||
"text": "K",
|
||||
"color": "#00007f",
|
||||
"fontSize": 24.0,
|
||||
"lineStyle": "solid",
|
||||
"lineWidth": 1.5,
|
||||
"stroke": "#00007f",
|
||||
"fill": "#ffffff"
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"showName": false
|
||||
},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": "out = k*in;",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "parameter-61a43a86",
|
||||
"name": "k",
|
||||
"type": "real",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "12b21b11-8828-4a90-a561-19491dc9632e",
|
||||
"name": "differentiate",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-1cbabc8f",
|
||||
"name": "in",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 64.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-4eeea4e7",
|
||||
"name": "out",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 88.0,
|
||||
"y": 40.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "Text",
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 128.0
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"x": 32.0,
|
||||
"y": 32.0,
|
||||
"width": 64.0,
|
||||
"height": 64.0,
|
||||
"fill": "#f4f4f4",
|
||||
"stroke": "#303030",
|
||||
"lineWidth": 1.5,
|
||||
"lineStyle": "solid",
|
||||
"cornerRadius": 5.0
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 40.0,
|
||||
"y": 40.0,
|
||||
"width": 48.0,
|
||||
"height": 48.0,
|
||||
"text": "d/dt",
|
||||
"color": "#00007f",
|
||||
"fontSize": 18.0,
|
||||
"lineStyle": "solid",
|
||||
"lineWidth": 1.5,
|
||||
"stroke": "#00007f",
|
||||
"fill": "#ffffff"
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"showName": false
|
||||
},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": "initial out = initial;\nout = der(in);",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "parameter-331e38be",
|
||||
"name": "initial",
|
||||
"type": "real",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "afdc7696-622b-47e4-b2c3-28ec60786bf2",
|
||||
"name": "integrate",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-12903207",
|
||||
"name": "in",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 64.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-b3370b1a",
|
||||
"name": "out",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 88.0,
|
||||
"y": 40.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "Text",
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 128.0
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"x": 32.0,
|
||||
"y": 32.0,
|
||||
"width": 64.0,
|
||||
"height": 64.0,
|
||||
"fill": "#f4f4f4",
|
||||
"stroke": "#303030",
|
||||
"lineWidth": 1.5,
|
||||
"lineStyle": "solid",
|
||||
"cornerRadius": 5.0
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 64.0,
|
||||
"y": 40.0,
|
||||
"width": 28.0,
|
||||
"height": 48.0,
|
||||
"text": "dt",
|
||||
"color": "#00007f",
|
||||
"fontSize": 18.0,
|
||||
"lineStyle": "solid",
|
||||
"lineWidth": 1.5,
|
||||
"stroke": "#00007f",
|
||||
"fill": "#ffffff"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 40.0,
|
||||
"y": 32.0,
|
||||
"width": 28.0,
|
||||
"height": 54.0,
|
||||
"fill": "none",
|
||||
"stroke": "#00007f",
|
||||
"lineWidth": 1.5,
|
||||
"lineStyle": "none",
|
||||
"text": "\u222b",
|
||||
"fontSize": 32.0,
|
||||
"color": "#00007f"
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"showName": false
|
||||
},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": "initial out = initial;\nder(out) = in;",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "parameter-6bdc1c76",
|
||||
"name": "initial",
|
||||
"type": "real",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "f6b8e5c9-4d13-4f7b-b574-e00ca96e42c3",
|
||||
"name": "add",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-c995845c",
|
||||
"name": "in",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 64.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": true
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-a2f0dea6",
|
||||
"name": "out",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 80.0,
|
||||
"y": 48.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "Text",
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 128.0
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"type": "circle",
|
||||
"x": 40.0,
|
||||
"y": 40.0,
|
||||
"width": 48.0,
|
||||
"height": 48.0,
|
||||
"fill": "#f4f4f4",
|
||||
"stroke": "#303030",
|
||||
"lineWidth": 1.5,
|
||||
"lineStyle": "solid"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 48.0,
|
||||
"y": 48.0,
|
||||
"width": 32.0,
|
||||
"height": 32.0,
|
||||
"fill": "none",
|
||||
"stroke": "#00007f",
|
||||
"lineWidth": 1.5,
|
||||
"lineStyle": "none",
|
||||
"text": "+",
|
||||
"fontSize": 18.0,
|
||||
"color": "#00007f"
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"showName": false
|
||||
},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": "out = sum(in[i] for i in 1:in.N);",
|
||||
"parameters": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
{
|
||||
"format": "bedit-document",
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"name": "Example"
|
||||
},
|
||||
"roots": [
|
||||
{
|
||||
"id": "bf714517-e7b2-4f4b-8b53-55642e3beb28",
|
||||
"name": "A",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-5c5d4695",
|
||||
"name": "Port 1",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 32.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-de109124",
|
||||
"name": "Port 2",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 96.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal"
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "Text",
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 128.0
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"x": 32.0,
|
||||
"y": 32.0,
|
||||
"width": 64.0,
|
||||
"height": 64.0,
|
||||
"fill": "#f4f4f4",
|
||||
"stroke": "#303030",
|
||||
"lineWidth": 1.5,
|
||||
"lineStyle": "solid",
|
||||
"cornerRadius": 5.0
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 40.0,
|
||||
"y": 40.0,
|
||||
"width": 48.0,
|
||||
"height": 48.0,
|
||||
"text": "A",
|
||||
"color": "#303030",
|
||||
"fontSize": 12.0,
|
||||
"lineStyle": "solid",
|
||||
"lineWidth": 1.5,
|
||||
"stroke": "#303030",
|
||||
"fill": "#ffffff"
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": [],
|
||||
"parameters": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "73c9d0c0-293d-4a55-8b89-a1f095dfa75f",
|
||||
"name": "B",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-4f732b3e",
|
||||
"name": "Port 1",
|
||||
"position": {
|
||||
"x": -176.0,
|
||||
"y": -144.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 32.0,
|
||||
"y": 48.0
|
||||
}
|
||||
},
|
||||
"type": "signal"
|
||||
},
|
||||
{
|
||||
"id": "port-ef7c4218",
|
||||
"name": "Port 2",
|
||||
"position": {
|
||||
"x": -176.0,
|
||||
"y": -16.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 32.0,
|
||||
"y": 80.0
|
||||
}
|
||||
},
|
||||
"type": "signal"
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-b68679b2",
|
||||
"name": "Port 3",
|
||||
"position": {
|
||||
"x": 128.0,
|
||||
"y": -80.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 96.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal"
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "Graph",
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 128.0
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"type": "rectangle",
|
||||
"x": 32.0,
|
||||
"y": 32.0,
|
||||
"width": 64.0,
|
||||
"height": 64.0,
|
||||
"fill": "#f4f4f4",
|
||||
"stroke": "#303030",
|
||||
"lineWidth": 1.5,
|
||||
"lineStyle": "solid",
|
||||
"cornerRadius": 5.0
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 40.0,
|
||||
"y": 40.0,
|
||||
"width": 48.0,
|
||||
"height": 48.0,
|
||||
"text": "B",
|
||||
"color": "#303030",
|
||||
"fontSize": 12.0,
|
||||
"lineStyle": "solid",
|
||||
"lineWidth": 1.5,
|
||||
"stroke": "#303030",
|
||||
"fill": "#ffffff"
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "graph",
|
||||
"graph": {
|
||||
"blocks": [],
|
||||
"connections": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
from PySide6.QtCore import QPointF
|
||||
from PySide6.QtGui import QUndoCommand
|
||||
|
||||
from bedit.core.model import Annotation, Component, Connection, Port
|
||||
from bedit.core.model import Annotation, Component, Connection, Junction, Port
|
||||
|
||||
|
||||
class AddComponentCommand(QUndoCommand):
|
||||
@@ -76,6 +76,32 @@ class AddConnectionCommand(QUndoCommand):
|
||||
self.controller._remove_connection(self.owner_id, self.connection.id)
|
||||
|
||||
|
||||
class SplitConnectionCommand(QUndoCommand):
|
||||
def __init__(
|
||||
self,
|
||||
controller,
|
||||
owner_id: str,
|
||||
original: Connection,
|
||||
junction: Junction,
|
||||
first: Connection,
|
||||
second: Connection,
|
||||
) -> None:
|
||||
super().__init__("Add connection junction")
|
||||
self.controller, self.owner_id = controller, owner_id
|
||||
self.original, self.junction = original, junction
|
||||
self.first, self.second = first, second
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._split_connection(
|
||||
self.owner_id, self.original.id, self.junction, self.first, self.second
|
||||
)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._restore_split_connection(
|
||||
self.owner_id, self.original, self.junction.id, self.first.id, self.second.id
|
||||
)
|
||||
|
||||
|
||||
class AddAnnotationCommand(QUndoCommand):
|
||||
def __init__(self, controller, owner_id: str, annotation: Annotation) -> None:
|
||||
super().__init__(f"Draw {annotation.kind}")
|
||||
@@ -200,6 +226,19 @@ class EditComponentAppearanceCommand(QUndoCommand):
|
||||
self.controller._set_component_appearance(self.component_id, self.old)
|
||||
|
||||
|
||||
class EditComponentPropertiesCommand(QUndoCommand):
|
||||
def __init__(self, controller, component_id: str, old: dict, new: dict, text: str) -> None:
|
||||
super().__init__(text)
|
||||
self.controller, self.component_id = controller, component_id
|
||||
self.old, self.new = old, new
|
||||
|
||||
def redo(self) -> None:
|
||||
self.controller._set_component_properties(self.component_id, self.new)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._set_component_properties(self.component_id, self.old)
|
||||
|
||||
|
||||
class RenameInterfacePortCommand(QUndoCommand):
|
||||
def __init__(self, controller, owner_id: str, port_id: str, old: str, new: str) -> None:
|
||||
super().__init__("Rename interface port")
|
||||
@@ -309,3 +348,14 @@ class EditTextDefinitionCommand(QUndoCommand):
|
||||
|
||||
def undo(self) -> None:
|
||||
self.controller._set_text_definition(self.component_id, self.old)
|
||||
|
||||
def id(self) -> int:
|
||||
return 1001
|
||||
|
||||
def mergeWith(self, other: QUndoCommand) -> bool: # noqa: N802
|
||||
if not isinstance(other, EditTextDefinitionCommand):
|
||||
return False
|
||||
if other.component_id != self.component_id:
|
||||
return False
|
||||
self.new = other.new
|
||||
return True
|
||||
|
||||
@@ -15,6 +15,7 @@ from bedit.gui.controllers.commands import (
|
||||
EditGraphItemCommand,
|
||||
EditTextDefinitionCommand,
|
||||
EditComponentAppearanceCommand,
|
||||
EditComponentPropertiesCommand,
|
||||
MoveComponentCommand,
|
||||
MoveInterfacePortCommand,
|
||||
PasteSelectionCommand,
|
||||
@@ -22,6 +23,7 @@ from bedit.gui.controllers.commands import (
|
||||
RenameInterfacePortCommand,
|
||||
ReplaceSourceCommand,
|
||||
RotateComponentsCommand,
|
||||
SplitConnectionCommand,
|
||||
)
|
||||
from bedit.core.model import (
|
||||
Annotation,
|
||||
@@ -30,6 +32,8 @@ from bedit.core.model import (
|
||||
Endpoint,
|
||||
GraphDocument,
|
||||
Icon,
|
||||
Junction,
|
||||
Parameter,
|
||||
Port,
|
||||
clone_component,
|
||||
)
|
||||
@@ -45,6 +49,8 @@ class DocumentController(QObject):
|
||||
componentRemoved = Signal(str)
|
||||
componentMoved = Signal(str, QPointF)
|
||||
componentRotated = Signal(str, float)
|
||||
componentPropertiesChanged = Signal(str)
|
||||
textDefinitionChanged = Signal(str)
|
||||
connectionAdded = Signal(str)
|
||||
connectionRemoved = Signal(str)
|
||||
graphItemChanged = Signal(str, str)
|
||||
@@ -151,12 +157,13 @@ class DocumentController(QObject):
|
||||
if self.document is None:
|
||||
raise ValueError("Open or create a document first")
|
||||
number = len(self.document.roots) + 1
|
||||
base_name = f"New {'Graph' if kind == 'graph' else 'Text'} Block "
|
||||
component = Component(
|
||||
id=str(uuid4()),
|
||||
name=f"New {'Graph' if kind == 'graph' else 'Text'} Block {number}",
|
||||
name=self._available_component_name(base_name, self.document.roots.values(), number),
|
||||
implementation_kind=kind,
|
||||
icon=Icon(text="Graph" if kind == "graph" else "Text"),
|
||||
source={"equations": [], "parameters": {}} if kind == "text" else {},
|
||||
source={"equations": "", "parameters": []} if kind == "text" else {},
|
||||
)
|
||||
self.undo_stack.push(AddComponentCommand(self, None, component))
|
||||
self.activate_component(component.id)
|
||||
@@ -169,12 +176,13 @@ class DocumentController(QObject):
|
||||
if owner is None or owner.implementation_kind != "graph":
|
||||
raise ValueError("Children can only be added to graph components")
|
||||
number = len(owner.graph.blocks) + 1
|
||||
base_name = f"New {'Graph' if kind == 'graph' else 'Text'} Block "
|
||||
component = Component(
|
||||
id=str(uuid4()),
|
||||
name=f"New {'Graph' if kind == 'graph' else 'Text'} Block {number}",
|
||||
name=self._available_component_name(base_name, owner.graph.blocks.values(), number),
|
||||
implementation_kind=kind,
|
||||
icon=Icon(text="Graph" if kind == "graph" else "Text"),
|
||||
source={"equations": [], "parameters": {}} if kind == "text" else {},
|
||||
source={"equations": "", "parameters": []} if kind == "text" else {},
|
||||
)
|
||||
self.undo_stack.push(AddComponentCommand(self, owner_id, component))
|
||||
return component.id
|
||||
@@ -209,6 +217,9 @@ class DocumentController(QObject):
|
||||
if self.active_component is None or self.active_component.implementation_kind != "graph":
|
||||
raise ValueError("Open a graph component before placing components")
|
||||
component = clone_component(source)
|
||||
component.name = self._available_component_name(
|
||||
source.name, self.active_component.graph.blocks.values(), 0
|
||||
)
|
||||
component.x, component.y = position.x(), position.y()
|
||||
self.undo_stack.push(AddComponentCommand(self, self.active_component_id, component))
|
||||
return component.id
|
||||
@@ -219,6 +230,20 @@ class DocumentController(QObject):
|
||||
MoveComponentCommand(self, self.active_component_id, component_id, old, new)
|
||||
)
|
||||
|
||||
def move_junction(self, junction_id: str, old: QPointF, new: QPointF) -> None:
|
||||
if old != new and self.active_component_id is not None:
|
||||
self.undo_stack.push(
|
||||
EditGraphItemCommand(
|
||||
self,
|
||||
self.active_component_id,
|
||||
"junction_geometry",
|
||||
junction_id,
|
||||
{"x": old.x(), "y": old.y()},
|
||||
{"x": new.x(), "y": new.y()},
|
||||
"Move connection junction",
|
||||
)
|
||||
)
|
||||
|
||||
def rotate_components(self, component_ids: set[str]) -> None:
|
||||
if self.active_component is None or self.active_component_id is None:
|
||||
return
|
||||
@@ -245,6 +270,14 @@ class DocumentController(QObject):
|
||||
raise ValueError("A connection endpoint no longer exists")
|
||||
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
|
||||
raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}")
|
||||
if not self.endpoint_accepts_connection(source, "source"):
|
||||
raise ValueError(
|
||||
f"Port {source_port.name!r} already has a connection; enable multiple connections first"
|
||||
)
|
||||
if not self.endpoint_accepts_connection(target, "target"):
|
||||
raise ValueError(
|
||||
f"Port {target_port.name!r} already has a connection; enable multiple connections first"
|
||||
)
|
||||
connection = Connection(
|
||||
str(uuid4()),
|
||||
source,
|
||||
@@ -256,6 +289,53 @@ class DocumentController(QObject):
|
||||
self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
|
||||
return connection.id
|
||||
|
||||
def split_connection(
|
||||
self,
|
||||
connection_id: str,
|
||||
position: QPointF,
|
||||
first_waypoints: list[QPointF],
|
||||
second_waypoints: list[QPointF],
|
||||
) -> str:
|
||||
if self.active_component_id is None:
|
||||
raise ValueError("There is no active graph")
|
||||
original = self.active_graph.connections.get(connection_id)
|
||||
if original is None:
|
||||
raise ValueError("The connection no longer exists")
|
||||
port_type = self.connection_port_type(original)
|
||||
junction = Junction(str(uuid4()), position.x(), position.y(), port_type)
|
||||
first_properties = deepcopy(original.properties)
|
||||
first_properties["waypoints"] = [
|
||||
{"x": point.x(), "y": point.y()} for point in first_waypoints
|
||||
]
|
||||
second_properties = {
|
||||
"waypoints": [{"x": point.x(), "y": point.y()} for point in second_waypoints]
|
||||
}
|
||||
first = Connection(
|
||||
str(uuid4()),
|
||||
original.source,
|
||||
Endpoint(junction=junction.id),
|
||||
original.name,
|
||||
first_properties,
|
||||
)
|
||||
second = Connection(
|
||||
str(uuid4()),
|
||||
Endpoint(junction=junction.id),
|
||||
original.target,
|
||||
"",
|
||||
second_properties,
|
||||
)
|
||||
self.undo_stack.push(
|
||||
SplitConnectionCommand(
|
||||
self,
|
||||
self.active_component_id,
|
||||
original,
|
||||
junction,
|
||||
first,
|
||||
second,
|
||||
)
|
||||
)
|
||||
return junction.id
|
||||
|
||||
def add_annotation(
|
||||
self,
|
||||
kind: str,
|
||||
@@ -396,6 +476,16 @@ class DocumentController(QObject):
|
||||
owner = self.active_component
|
||||
if owner is None:
|
||||
return None
|
||||
if endpoint.junction is not None:
|
||||
junction = owner.graph.junctions.get(endpoint.junction)
|
||||
if junction is None:
|
||||
return None
|
||||
return Port(
|
||||
junction.id,
|
||||
"Junction",
|
||||
type=junction.type,
|
||||
allows_multiple_connections=role == "source",
|
||||
)
|
||||
if endpoint.interface is not None:
|
||||
ports = owner.inputs if role == "source" else owner.outputs
|
||||
else:
|
||||
@@ -411,6 +501,17 @@ class DocumentController(QObject):
|
||||
port = self._port_for_endpoint(connection.source, "source")
|
||||
return port.type if port is not None else "signal"
|
||||
|
||||
def endpoint_accepts_connection(self, endpoint: Endpoint, role: str) -> bool:
|
||||
port = self._port_for_endpoint(endpoint, role)
|
||||
if port is None:
|
||||
return False
|
||||
if role == "source" or port.allows_multiple_connections:
|
||||
return True
|
||||
return not any(
|
||||
endpoint == (connection.source if role == "source" else connection.target)
|
||||
for connection in self.active_graph.connections.values()
|
||||
)
|
||||
|
||||
def add_interface_port(self, direction: str, position: QPointF) -> str:
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "graph":
|
||||
@@ -468,7 +569,8 @@ class DocumentController(QObject):
|
||||
self,
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
source: dict,
|
||||
equations: str,
|
||||
parameters: list[Parameter],
|
||||
) -> None:
|
||||
component = self.active_component
|
||||
if component is None or component.implementation_kind != "text":
|
||||
@@ -477,6 +579,13 @@ class DocumentController(QObject):
|
||||
output_ids = [port.id for port in outputs]
|
||||
if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids):
|
||||
raise ValueError("Input and output IDs must be unique")
|
||||
if any(not port.name.strip() for port in (*inputs, *outputs)):
|
||||
raise ValueError("Every port must have a name")
|
||||
parameter_ids = [parameter.id for parameter in parameters]
|
||||
if len(set(parameter_ids)) != len(parameter_ids):
|
||||
raise ValueError("Parameter IDs must be unique")
|
||||
if any(not parameter.name.strip() for parameter in parameters):
|
||||
raise ValueError("Every parameter must have a name")
|
||||
if self.document is not None:
|
||||
parent = self.document.find_parent(component.id)
|
||||
if parent is not None:
|
||||
@@ -503,9 +612,18 @@ class DocumentController(QObject):
|
||||
new = {
|
||||
"inputs": [port.to_dict() for port in inputs],
|
||||
"outputs": [port.to_dict() for port in outputs],
|
||||
"source": deepcopy(source),
|
||||
"source": {
|
||||
"equations": equations,
|
||||
"parameters": [parameter.to_dict() for parameter in parameters],
|
||||
},
|
||||
}
|
||||
if old != new:
|
||||
candidate = deepcopy(self.document)
|
||||
candidate_component = candidate.find_component(component.id)
|
||||
candidate_component.inputs = deepcopy(inputs)
|
||||
candidate_component.outputs = deepcopy(outputs)
|
||||
candidate_component.source = deepcopy(new["source"])
|
||||
candidate.validate()
|
||||
self.undo_stack.push(EditTextDefinitionCommand(self, component.id, old, new))
|
||||
|
||||
def edit_component_appearance(
|
||||
@@ -516,25 +634,36 @@ class DocumentController(QObject):
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
show_subtree: bool,
|
||||
show_name: bool,
|
||||
) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
component = self.document.find_component(component_id)
|
||||
if component is None:
|
||||
return
|
||||
siblings = self._component_siblings(component_id)
|
||||
if any(item.id != component_id and item.name == name for item in siblings):
|
||||
raise ValueError(f"A component named {name!r} already exists at this level")
|
||||
old = {
|
||||
"name": component.name,
|
||||
"icon": component.icon.to_dict(),
|
||||
"inputs": [port.to_dict() for port in component.inputs],
|
||||
"outputs": [port.to_dict() for port in component.outputs],
|
||||
"show_subtree": component.show_subtree_in_library,
|
||||
"properties": deepcopy(component.properties),
|
||||
}
|
||||
properties = deepcopy(component.properties)
|
||||
was_visible = bool(properties.get("showName", False))
|
||||
properties["showName"] = show_name
|
||||
if show_name and not was_visible:
|
||||
properties.pop("nameLabelPosition", None)
|
||||
new = {
|
||||
"name": name,
|
||||
"icon": icon.to_dict(),
|
||||
"inputs": [port.to_dict() for port in inputs],
|
||||
"outputs": [port.to_dict() for port in outputs],
|
||||
"show_subtree": show_subtree,
|
||||
"properties": properties,
|
||||
}
|
||||
if old != new:
|
||||
candidate = deepcopy(self.document)
|
||||
@@ -543,9 +672,75 @@ class DocumentController(QObject):
|
||||
candidate_component.icon = Icon.from_dict(icon.to_dict())
|
||||
candidate_component.inputs = deepcopy(inputs)
|
||||
candidate_component.outputs = deepcopy(outputs)
|
||||
candidate_component.properties = deepcopy(properties)
|
||||
candidate.validate()
|
||||
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
|
||||
|
||||
def move_component_name_label(self, component_id: str, position: QPointF) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
component = self.document.find_component(component_id)
|
||||
if component is None:
|
||||
return
|
||||
old = deepcopy(component.properties)
|
||||
new = deepcopy(old)
|
||||
new["nameLabelPosition"] = {"x": position.x(), "y": position.y()}
|
||||
if old != new:
|
||||
self.undo_stack.push(
|
||||
EditComponentPropertiesCommand(
|
||||
self, component_id, old, new, "Move component name"
|
||||
)
|
||||
)
|
||||
|
||||
def edit_connection_options(
|
||||
self, connection_id: str, name: str, show_name: bool
|
||||
) -> None:
|
||||
owner = self.active_component
|
||||
if owner is None or self.active_component_id is None:
|
||||
return
|
||||
connection = owner.graph.connections.get(connection_id)
|
||||
if connection is None:
|
||||
return
|
||||
old = {"name": connection.name, "properties": deepcopy(connection.properties)}
|
||||
properties = deepcopy(connection.properties)
|
||||
was_visible = bool(properties.get("showName", False))
|
||||
properties["showName"] = show_name
|
||||
if show_name and not was_visible:
|
||||
properties.pop("nameLabelPosition", None)
|
||||
new = {"name": name, "properties": properties}
|
||||
if old != new:
|
||||
self.undo_stack.push(
|
||||
EditGraphItemCommand(
|
||||
self,
|
||||
self.active_component_id,
|
||||
"connection_data",
|
||||
connection_id,
|
||||
old,
|
||||
new,
|
||||
"Edit connection options",
|
||||
)
|
||||
)
|
||||
|
||||
def move_connection_name_label(self, connection_id: str, position: QPointF) -> None:
|
||||
connection = self.active_graph.connections.get(connection_id)
|
||||
if connection is None or self.active_component_id is None:
|
||||
return
|
||||
old = deepcopy(connection.properties)
|
||||
new = deepcopy(old)
|
||||
new["nameLabelPosition"] = {"x": position.x(), "y": position.y()}
|
||||
if old != new:
|
||||
self.undo_stack.push(
|
||||
EditGraphItemCommand(
|
||||
self,
|
||||
self.active_component_id,
|
||||
"connection",
|
||||
connection_id,
|
||||
old,
|
||||
new,
|
||||
"Move connection name",
|
||||
)
|
||||
)
|
||||
|
||||
def edit_component_ports(
|
||||
self, component_id: str, inputs: list[Port], outputs: list[Port]
|
||||
) -> None:
|
||||
@@ -585,6 +780,7 @@ class DocumentController(QObject):
|
||||
"inputs": [port.to_dict() for port in component.inputs],
|
||||
"outputs": [port.to_dict() for port in component.outputs],
|
||||
"show_subtree": component.show_subtree_in_library,
|
||||
"properties": deepcopy(component.properties),
|
||||
}
|
||||
new = {
|
||||
**old,
|
||||
@@ -649,10 +845,13 @@ class DocumentController(QObject):
|
||||
pairs = [(source, clone_component(source)) for source in source_components]
|
||||
id_map = {source.id: clone.id for source, clone in pairs}
|
||||
blocks = {}
|
||||
for _source, clone in pairs:
|
||||
used = list(owner.graph.blocks.values())
|
||||
for source, clone in pairs:
|
||||
clone.name = self._available_component_name(source.name, used, 0)
|
||||
clone.x += offset.x()
|
||||
clone.y += offset.y()
|
||||
blocks[clone.id] = clone
|
||||
used.append(clone)
|
||||
connections = {}
|
||||
for source in source_connections:
|
||||
if source.source.block not in id_map or source.target.block not in id_map:
|
||||
@@ -682,6 +881,26 @@ class DocumentController(QObject):
|
||||
raise ValueError("The containing component is no longer in the document")
|
||||
return owner.graph
|
||||
|
||||
@staticmethod
|
||||
def _available_component_name(
|
||||
base: str, components, start: int = 0
|
||||
) -> str:
|
||||
used = {component.name for component in components}
|
||||
number = start
|
||||
while f"{base}{number}" in used:
|
||||
number += 1
|
||||
return f"{base}{number}"
|
||||
|
||||
def _component_siblings(self, component_id: str):
|
||||
if self.document is None:
|
||||
return ()
|
||||
parent = self.document.find_parent(component_id)
|
||||
return (
|
||||
parent.graph.blocks.values()
|
||||
if parent is not None
|
||||
else self.document.roots.values()
|
||||
)
|
||||
|
||||
def _insert_component(self, owner_id: str | None, component: Component) -> None:
|
||||
if self.document is None:
|
||||
raise ValueError("There is no open document")
|
||||
@@ -728,6 +947,36 @@ class DocumentController(QObject):
|
||||
self.connectionAdded.emit(connection.id)
|
||||
self.documentReset.emit()
|
||||
|
||||
def _split_connection(
|
||||
self,
|
||||
owner_id: str,
|
||||
original_id: str,
|
||||
junction: Junction,
|
||||
first: Connection,
|
||||
second: Connection,
|
||||
) -> None:
|
||||
graph = self._graph_for(owner_id)
|
||||
graph.connections.pop(original_id, None)
|
||||
graph.junctions[junction.id] = junction
|
||||
graph.connections[first.id] = first
|
||||
graph.connections[second.id] = second
|
||||
self.documentReset.emit()
|
||||
|
||||
def _restore_split_connection(
|
||||
self,
|
||||
owner_id: str,
|
||||
original: Connection,
|
||||
junction_id: str,
|
||||
first_id: str,
|
||||
second_id: str,
|
||||
) -> None:
|
||||
graph = self._graph_for(owner_id)
|
||||
graph.connections.pop(first_id, None)
|
||||
graph.connections.pop(second_id, None)
|
||||
graph.junctions.pop(junction_id, None)
|
||||
graph.connections[original.id] = original
|
||||
self.documentReset.emit()
|
||||
|
||||
def _remove_connection(self, owner_id: str, connection_id: str) -> None:
|
||||
self._graph_for(owner_id).connections.pop(connection_id, None)
|
||||
if owner_id == self.active_component_id:
|
||||
@@ -750,7 +999,16 @@ class DocumentController(QObject):
|
||||
self, owner_id: str, item_kind: str, item_id: str, values: dict
|
||||
) -> None:
|
||||
graph = self._graph_for(owner_id)
|
||||
if item_kind == "connection":
|
||||
if item_kind == "junction_geometry":
|
||||
item = graph.junctions.get(item_id)
|
||||
if item is not None:
|
||||
item.x, item.y = float(values["x"]), float(values["y"])
|
||||
elif item_kind == "connection_data":
|
||||
item = graph.connections.get(item_id)
|
||||
if item is not None:
|
||||
item.name = values["name"]
|
||||
item.properties = deepcopy(values["properties"])
|
||||
elif item_kind == "connection":
|
||||
item = graph.connections.get(item_id)
|
||||
if item is not None:
|
||||
item.properties = deepcopy(values)
|
||||
@@ -867,10 +1125,19 @@ class DocumentController(QObject):
|
||||
component.inputs = [Port.from_dict(port) for port in values["inputs"]]
|
||||
component.outputs = [Port.from_dict(port) for port in values["outputs"]]
|
||||
component.show_subtree_in_library = values["show_subtree"]
|
||||
component.properties = deepcopy(values["properties"])
|
||||
self.documentReset.emit()
|
||||
if component_id == self.active_component_id:
|
||||
self.activeGraphChanged.emit()
|
||||
|
||||
def _set_component_properties(self, component_id: str, properties: dict) -> None:
|
||||
if self.document is None:
|
||||
return
|
||||
component = self.document.find_component(component_id)
|
||||
if component is not None:
|
||||
component.properties = deepcopy(properties)
|
||||
self.componentPropertiesChanged.emit(component_id)
|
||||
|
||||
def _delete_items(
|
||||
self,
|
||||
owner_id: str | None,
|
||||
@@ -948,5 +1215,4 @@ class DocumentController(QObject):
|
||||
component.source = deepcopy(values["source"])
|
||||
self.interfaceChanged.emit()
|
||||
self.documentReset.emit()
|
||||
if component_id == self.active_component_id:
|
||||
self.activeGraphChanged.emit()
|
||||
self.textDefinitionChanged.emit(component_id)
|
||||
|
||||
@@ -17,6 +17,7 @@ class ComponentOptionsDialog(QDialog):
|
||||
self.ui.nameEdit.setText(component.name)
|
||||
self.ui.editIconButton.clicked.connect(self.edit_icon)
|
||||
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
|
||||
self.ui.showNameCheckBox.setChecked(bool(component.properties.get("showName", False)))
|
||||
|
||||
def edit_icon(self) -> None:
|
||||
working = Component.from_dict(self.component.to_dict())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QCheckBox,
|
||||
QFormLayout,
|
||||
QLineEdit,
|
||||
QMessageBox,
|
||||
@@ -11,7 +12,15 @@ from PySide6.QtWidgets import (
|
||||
class ItemOptionsDialog(QDialog):
|
||||
"""Small, extensible options dialog shared by ports and connections."""
|
||||
|
||||
def __init__(self, title: str, name: str, parent=None, *, name_required: bool = True) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
name: str,
|
||||
parent=None,
|
||||
*,
|
||||
name_required: bool = True,
|
||||
show_name: bool | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.name_required = name_required
|
||||
self.setWindowTitle(title)
|
||||
@@ -21,6 +30,11 @@ class ItemOptionsDialog(QDialog):
|
||||
self.form = QFormLayout()
|
||||
self.name_edit = QLineEdit(name, self)
|
||||
self.form.addRow("Name:", self.name_edit)
|
||||
self.show_name_check = None
|
||||
if show_name is not None:
|
||||
self.show_name_check = QCheckBox("Show name below connection", self)
|
||||
self.show_name_check.setChecked(show_name)
|
||||
self.form.addRow("", self.show_name_check)
|
||||
layout.addLayout(self.form)
|
||||
|
||||
buttons = QDialogButtonBox(
|
||||
@@ -35,6 +49,10 @@ class ItemOptionsDialog(QDialog):
|
||||
def name(self) -> str:
|
||||
return self.name_edit.text().strip()
|
||||
|
||||
@property
|
||||
def show_name(self) -> bool:
|
||||
return bool(self.show_name_check and self.show_name_check.isChecked())
|
||||
|
||||
def accept(self) -> None:
|
||||
if self.name_required and not self.name:
|
||||
QMessageBox.warning(self, "Invalid name", "The name cannot be empty.")
|
||||
|
||||
@@ -36,6 +36,7 @@ class PortOptionsDialog(QDialog):
|
||||
self.ui.nameEdit.textEdited.connect(self._store_current)
|
||||
self.ui.typeCombo.currentIndexChanged.connect(self._store_current)
|
||||
self.ui.orientationCombo.currentIndexChanged.connect(self._store_current)
|
||||
self.ui.multipleConnectionsCheckBox.toggled.connect(self._store_current)
|
||||
self.ui.portSplitter.setSizes([250, 370])
|
||||
if read_only:
|
||||
self.ui.addPortButton.setEnabled(False)
|
||||
@@ -43,6 +44,7 @@ class PortOptionsDialog(QDialog):
|
||||
self.ui.nameEdit.setReadOnly(True)
|
||||
self.ui.typeCombo.setEnabled(False)
|
||||
self.ui.orientationCombo.setEnabled(False)
|
||||
self.ui.multipleConnectionsCheckBox.setEnabled(False)
|
||||
self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("Close")
|
||||
self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).hide()
|
||||
self._rebuild_list(0 if self.ports else -1)
|
||||
@@ -72,6 +74,9 @@ class PortOptionsDialog(QDialog):
|
||||
self.ui.nameEdit.setText(port.name)
|
||||
self.ui.typeCombo.setCurrentIndex(self.ui.typeCombo.findData(port.type))
|
||||
self.ui.orientationCombo.setCurrentIndex(self.ui.orientationCombo.findData(orientation))
|
||||
self.ui.multipleConnectionsCheckBox.setChecked(
|
||||
port.allows_multiple_connections
|
||||
)
|
||||
else:
|
||||
self.ui.nameEdit.clear()
|
||||
self._loading = False
|
||||
@@ -83,6 +88,7 @@ class PortOptionsDialog(QDialog):
|
||||
self.ui.nameEdit.setEnabled(enabled)
|
||||
self.ui.typeCombo.setEnabled(enabled)
|
||||
self.ui.orientationCombo.setEnabled(enabled)
|
||||
self.ui.multipleConnectionsCheckBox.setEnabled(enabled)
|
||||
|
||||
def _store_current(self) -> None:
|
||||
row = self.ui.portList.currentRow()
|
||||
@@ -91,6 +97,7 @@ class PortOptionsDialog(QDialog):
|
||||
port, _orientation = self.ports[row]
|
||||
port.name = self.ui.nameEdit.text()
|
||||
port.type = self.ui.typeCombo.currentData()
|
||||
port.allows_multiple_connections = self.ui.multipleConnectionsCheckBox.isChecked()
|
||||
self.ports[row] = (port, self.ui.orientationCombo.currentData())
|
||||
self.ui.portList.item(row).setText(
|
||||
f"{port.name} [{self.ports[row][1]}, {port.type}]"
|
||||
|
||||
1
BEdit/src/bedit/gui/editors/__init__.py
Normal file
1
BEdit/src/bedit/gui/editors/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Reusable editing widgets."""
|
||||
199
BEdit/src/bedit/gui/editors/text_definition.py
Normal file
199
BEdit/src/bedit/gui/editors/text_definition.py
Normal file
@@ -0,0 +1,199 @@
|
||||
from copy import deepcopy
|
||||
from uuid import uuid4
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import QCheckBox, QComboBox, QHeaderView, QTableWidgetItem, QWidget
|
||||
|
||||
from bedit.core.model import Parameter, Port
|
||||
from bedit.core.port_types import PortTypeRegistry
|
||||
from bedit.gui.generated.ui_text_definition_editor import Ui_TextDefinitionEditor
|
||||
|
||||
|
||||
ID_ROLE = Qt.ItemDataRole.UserRole
|
||||
PROPERTIES_ROLE = Qt.ItemDataRole.UserRole + 1
|
||||
|
||||
|
||||
class TextDefinitionEditor(QWidget):
|
||||
"""Editor for a text component's equations, ports, and parameters."""
|
||||
|
||||
modifiedChanged = Signal(bool)
|
||||
definitionEdited = Signal()
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.ui = Ui_TextDefinitionEditor()
|
||||
self.ui.setupUi(self)
|
||||
self._modified = False
|
||||
self._loading = False
|
||||
self.ui.portsTable.horizontalHeader().setSectionResizeMode(
|
||||
QHeaderView.ResizeMode.Stretch
|
||||
)
|
||||
self.ui.parametersTable.horizontalHeader().setSectionResizeMode(
|
||||
QHeaderView.ResizeMode.Stretch
|
||||
)
|
||||
self.ui.columnSplitter.setSizes([560, 340])
|
||||
self.ui.definitionSplitter.setSizes([300, 300])
|
||||
self.ui.equationsEdit.textChanged.connect(self._mark_modified)
|
||||
self.ui.portsTable.cellChanged.connect(self._mark_modified)
|
||||
self.ui.parametersTable.cellChanged.connect(self._mark_modified)
|
||||
self.ui.addPortButton.clicked.connect(self.add_port)
|
||||
self.ui.removePortButton.clicked.connect(self.remove_port)
|
||||
self.ui.addParameterButton.clicked.connect(self.add_parameter)
|
||||
self.ui.removeParameterButton.clicked.connect(self.remove_parameter)
|
||||
self.ui.portsTable.itemSelectionChanged.connect(self._update_buttons)
|
||||
self.ui.parametersTable.itemSelectionChanged.connect(self._update_buttons)
|
||||
self._update_buttons()
|
||||
|
||||
@property
|
||||
def is_modified(self) -> bool:
|
||||
return self._modified
|
||||
|
||||
@property
|
||||
def equations(self) -> str:
|
||||
return self.ui.equationsEdit.toPlainText()
|
||||
|
||||
@property
|
||||
def ports(self) -> tuple[list[Port], list[Port]]:
|
||||
inputs: list[Port] = []
|
||||
outputs: list[Port] = []
|
||||
for row in range(self.ui.portsTable.rowCount()):
|
||||
name_item = self.ui.portsTable.item(row, 0)
|
||||
type_combo = self.ui.portsTable.cellWidget(row, 1)
|
||||
orientation_combo = self.ui.portsTable.cellWidget(row, 2)
|
||||
multiple_check = self.ui.portsTable.cellWidget(row, 3)
|
||||
port = Port(
|
||||
id=name_item.data(ID_ROLE),
|
||||
name=name_item.text().strip(),
|
||||
type=type_combo.currentData(),
|
||||
properties=deepcopy(name_item.data(PROPERTIES_ROLE) or {}),
|
||||
allows_multiple_connections=multiple_check.isChecked(),
|
||||
)
|
||||
target = inputs if orientation_combo.currentData() == "input" else outputs
|
||||
target.append(port)
|
||||
return inputs, outputs
|
||||
|
||||
@property
|
||||
def parameters(self) -> list[Parameter]:
|
||||
table = self.ui.parametersTable
|
||||
return [
|
||||
Parameter(
|
||||
id=table.item(row, 0).data(ID_ROLE),
|
||||
name=table.item(row, 0).text().strip(),
|
||||
type=table.item(row, 1).text().strip(),
|
||||
value=table.item(row, 2).text(),
|
||||
)
|
||||
for row in range(table.rowCount())
|
||||
]
|
||||
|
||||
def set_definition(
|
||||
self,
|
||||
equations: str,
|
||||
inputs: list[Port],
|
||||
outputs: list[Port],
|
||||
parameters: list[Parameter],
|
||||
) -> None:
|
||||
self._loading = True
|
||||
self.ui.equationsEdit.setPlainText(equations)
|
||||
self.ui.portsTable.setRowCount(0)
|
||||
for port in inputs:
|
||||
self._append_port(port, "input")
|
||||
for port in outputs:
|
||||
self._append_port(port, "output")
|
||||
self.ui.parametersTable.setRowCount(0)
|
||||
for parameter in parameters:
|
||||
self._append_parameter(parameter)
|
||||
self._loading = False
|
||||
self.set_modified(False)
|
||||
self._update_buttons()
|
||||
|
||||
def set_modified(self, modified: bool) -> None:
|
||||
if self._modified != modified:
|
||||
self._modified = modified
|
||||
self.modifiedChanged.emit(modified)
|
||||
|
||||
def _mark_modified(self, *_args) -> None:
|
||||
if not self._loading:
|
||||
self.set_modified(True)
|
||||
self.definitionEdited.emit()
|
||||
|
||||
def _new_combo(self, values: list[tuple[str, str]], current: str) -> QComboBox:
|
||||
combo = QComboBox(self)
|
||||
for label, value in values:
|
||||
combo.addItem(label, value)
|
||||
combo.setCurrentIndex(max(0, combo.findData(current)))
|
||||
combo.currentIndexChanged.connect(self._mark_modified)
|
||||
return combo
|
||||
|
||||
def _append_port(self, port: Port, orientation: str) -> None:
|
||||
table = self.ui.portsTable
|
||||
row = table.rowCount()
|
||||
table.insertRow(row)
|
||||
name = QTableWidgetItem(port.name)
|
||||
name.setData(ID_ROLE, port.id)
|
||||
name.setData(PROPERTIES_ROLE, deepcopy(port.properties))
|
||||
table.setItem(row, 0, name)
|
||||
types = [(item.display_name, item.id) for item in PortTypeRegistry.all()]
|
||||
table.setCellWidget(row, 1, self._new_combo(types, port.type))
|
||||
table.setCellWidget(
|
||||
row,
|
||||
2,
|
||||
self._new_combo([("Input", "input"), ("Output", "output")], orientation),
|
||||
)
|
||||
multiple = QCheckBox("Any", self)
|
||||
multiple.setChecked(port.allows_multiple_connections)
|
||||
multiple.toggled.connect(self._mark_modified)
|
||||
table.setCellWidget(row, 3, multiple)
|
||||
|
||||
def _append_parameter(self, parameter: Parameter) -> None:
|
||||
table = self.ui.parametersTable
|
||||
row = table.rowCount()
|
||||
table.insertRow(row)
|
||||
name = QTableWidgetItem(parameter.name)
|
||||
name.setData(ID_ROLE, parameter.id)
|
||||
table.setItem(row, 0, name)
|
||||
table.setItem(row, 1, QTableWidgetItem(parameter.type))
|
||||
table.setItem(row, 2, QTableWidgetItem(parameter.value))
|
||||
|
||||
def add_port(self) -> None:
|
||||
port = Port(
|
||||
id=f"port-{uuid4().hex[:8]}",
|
||||
name=f"Port {self.ui.portsTable.rowCount() + 1}",
|
||||
type="signal",
|
||||
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
|
||||
)
|
||||
self._loading = True
|
||||
self._append_port(port, "input")
|
||||
self._loading = False
|
||||
self.ui.portsTable.selectRow(self.ui.portsTable.rowCount() - 1)
|
||||
self._mark_modified()
|
||||
|
||||
def remove_port(self) -> None:
|
||||
row = self.ui.portsTable.currentRow()
|
||||
if row >= 0:
|
||||
self.ui.portsTable.removeRow(row)
|
||||
self._mark_modified()
|
||||
self._update_buttons()
|
||||
|
||||
def add_parameter(self) -> None:
|
||||
parameter = Parameter(
|
||||
id=f"parameter-{uuid4().hex[:8]}",
|
||||
name=f"Parameter {self.ui.parametersTable.rowCount() + 1}",
|
||||
)
|
||||
self._loading = True
|
||||
self._append_parameter(parameter)
|
||||
self._loading = False
|
||||
self.ui.parametersTable.selectRow(self.ui.parametersTable.rowCount() - 1)
|
||||
self._mark_modified()
|
||||
|
||||
def remove_parameter(self) -> None:
|
||||
row = self.ui.parametersTable.currentRow()
|
||||
if row >= 0:
|
||||
self.ui.parametersTable.removeRow(row)
|
||||
self._mark_modified()
|
||||
self._update_buttons()
|
||||
|
||||
def _update_buttons(self) -> None:
|
||||
self.ui.removePortButton.setEnabled(self.ui.portsTable.currentRow() >= 0)
|
||||
self.ui.removeParameterButton.setEnabled(
|
||||
self.ui.parametersTable.currentRow() >= 0
|
||||
)
|
||||
@@ -55,6 +55,11 @@ class Ui_ComponentOptionsDialog(object):
|
||||
|
||||
self.optionsForm.setWidget(2, QFormLayout.ItemRole.SpanningRole, self.showSubtreeCheckBox)
|
||||
|
||||
self.showNameCheckBox = QCheckBox(ComponentOptionsDialog)
|
||||
self.showNameCheckBox.setObjectName(u"showNameCheckBox")
|
||||
|
||||
self.optionsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.showNameCheckBox)
|
||||
|
||||
|
||||
self.dialogLayout.addLayout(self.optionsForm)
|
||||
|
||||
@@ -85,5 +90,6 @@ class Ui_ComponentOptionsDialog(object):
|
||||
self.editIconButton.setToolTip(QCoreApplication.translate("ComponentOptionsDialog", u"Open the vector icon and port-position editor", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.showSubtreeCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show contained components in the Libraries tree", None))
|
||||
self.showNameCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show name below component", None))
|
||||
# retranslateUi
|
||||
|
||||
|
||||
@@ -18,10 +18,11 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
|
||||
QTransform)
|
||||
from PySide6.QtWidgets import (QApplication, QDockWidget, QFrame, QHBoxLayout,
|
||||
QHeaderView, QLabel, QMainWindow, QMenu,
|
||||
QMenuBar, QPlainTextEdit, QPushButton, QSizePolicy,
|
||||
QSpacerItem, QSplitter, QStackedWidget, QToolBar,
|
||||
QToolButton, QTreeView, QVBoxLayout, QWidget)
|
||||
QMenuBar, QSizePolicy, QSpacerItem, QSplitter,
|
||||
QStackedWidget, QToolBar, QToolButton, QTreeView,
|
||||
QVBoxLayout, QWidget)
|
||||
|
||||
from bedit.gui.editors.text_definition import TextDefinitionEditor
|
||||
from bedit.gui.graphics.workspace import GraphWorkspaceView
|
||||
from . import resources_rc
|
||||
|
||||
@@ -60,6 +61,8 @@ class Ui_MainWindow(object):
|
||||
icon5 = QIcon()
|
||||
icon5.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.actionOpen.setIcon(icon5)
|
||||
self.actionReloadLibraries = QAction(MainWindow)
|
||||
self.actionReloadLibraries.setObjectName(u"actionReloadLibraries")
|
||||
self.actionSave = QAction(MainWindow)
|
||||
self.actionSave.setObjectName(u"actionSave")
|
||||
icon6 = QIcon()
|
||||
@@ -217,12 +220,6 @@ class Ui_MainWindow(object):
|
||||
|
||||
self.workspaceHeaderLayout.addItem(self.workspaceHeaderSpacer)
|
||||
|
||||
self.applyJsonButton = QPushButton(self.workspaceHeader)
|
||||
self.applyJsonButton.setObjectName(u"applyJsonButton")
|
||||
self.applyJsonButton.setVisible(False)
|
||||
|
||||
self.workspaceHeaderLayout.addWidget(self.applyJsonButton)
|
||||
|
||||
self.pointerToolButton = QToolButton(self.workspaceHeader)
|
||||
self.pointerToolButton.setObjectName(u"pointerToolButton")
|
||||
icon15 = QIcon()
|
||||
@@ -291,18 +288,17 @@ class Ui_MainWindow(object):
|
||||
self.graphPageLayout.addWidget(self.graphView)
|
||||
|
||||
self.workspaceStack.addWidget(self.graphPage)
|
||||
self.jsonPage = QWidget()
|
||||
self.jsonPage.setObjectName(u"jsonPage")
|
||||
self.jsonPageLayout = QVBoxLayout(self.jsonPage)
|
||||
self.jsonPageLayout.setObjectName(u"jsonPageLayout")
|
||||
self.jsonPageLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.jsonEditor = QPlainTextEdit(self.jsonPage)
|
||||
self.jsonEditor.setObjectName(u"jsonEditor")
|
||||
self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
||||
self.textPage = QWidget()
|
||||
self.textPage.setObjectName(u"textPage")
|
||||
self.textPageLayout = QVBoxLayout(self.textPage)
|
||||
self.textPageLayout.setObjectName(u"textPageLayout")
|
||||
self.textPageLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.textDefinitionEditor = TextDefinitionEditor(self.textPage)
|
||||
self.textDefinitionEditor.setObjectName(u"textDefinitionEditor")
|
||||
|
||||
self.jsonPageLayout.addWidget(self.jsonEditor)
|
||||
self.textPageLayout.addWidget(self.textDefinitionEditor)
|
||||
|
||||
self.workspaceStack.addWidget(self.jsonPage)
|
||||
self.workspaceStack.addWidget(self.textPage)
|
||||
self.emptyPage = QWidget()
|
||||
self.emptyPage.setObjectName(u"emptyPage")
|
||||
self.emptyPage.setStyleSheet(u"background-color: #9a9a9a;")
|
||||
@@ -364,6 +360,7 @@ class Ui_MainWindow(object):
|
||||
self.menubar.addAction(self.menuHelp.menuAction())
|
||||
self.menuFile.addAction(self.actionNew)
|
||||
self.menuFile.addAction(self.actionOpen)
|
||||
self.menuFile.addAction(self.actionReloadLibraries)
|
||||
self.menuFile.addSeparator()
|
||||
self.menuFile.addAction(self.actionSave)
|
||||
self.menuFile.addAction(self.actionSaveAs)
|
||||
@@ -439,6 +436,13 @@ class Ui_MainWindow(object):
|
||||
#endif // QT_CONFIG(statustip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionOpen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionReloadLibraries.setText(QCoreApplication.translate("MainWindow", u"Reload &Libraries", None))
|
||||
#if QT_CONFIG(statustip)
|
||||
self.actionReloadLibraries.setStatusTip(QCoreApplication.translate("MainWindow", u"Reload configured library files from disk", None))
|
||||
#endif // QT_CONFIG(statustip)
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionReloadLibraries.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionSave.setText(QCoreApplication.translate("MainWindow", u"&Save", None))
|
||||
#if QT_CONFIG(statustip)
|
||||
@@ -505,7 +509,6 @@ class Ui_MainWindow(object):
|
||||
self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None))
|
||||
self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None))
|
||||
self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None))
|
||||
self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None))
|
||||
self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None))
|
||||
self.connectToolButton.setText(QCoreApplication.translate("MainWindow", u"Connect", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
@@ -524,7 +527,6 @@ class Ui_MainWindow(object):
|
||||
self.rotateToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Rotate selected blocks clockwise", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.rotateToolButton.setText(QCoreApplication.translate("MainWindow", u"Rotate", None))
|
||||
self.jsonEditor.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Component JSON", None))
|
||||
self.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", None))
|
||||
self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None))
|
||||
self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"&Edit", None))
|
||||
|
||||
@@ -15,10 +15,11 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog,
|
||||
QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel,
|
||||
QLineEdit, QListWidget, QListWidgetItem, QPushButton,
|
||||
QSizePolicy, QSplitter, QVBoxLayout, QWidget)
|
||||
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox,
|
||||
QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout,
|
||||
QLabel, QLineEdit, QListWidget, QListWidgetItem,
|
||||
QPushButton, QSizePolicy, QSplitter, QVBoxLayout,
|
||||
QWidget)
|
||||
|
||||
class Ui_PortOptionsDialog(object):
|
||||
def setupUi(self, PortOptionsDialog):
|
||||
@@ -94,11 +95,16 @@ class Ui_PortOptionsDialog(object):
|
||||
|
||||
self.portDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.orientationCombo)
|
||||
|
||||
self.multipleConnectionsCheckBox = QCheckBox(self.portDetailsPanel)
|
||||
self.multipleConnectionsCheckBox.setObjectName(u"multipleConnectionsCheckBox")
|
||||
|
||||
self.portDetailsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.multipleConnectionsCheckBox)
|
||||
|
||||
self.positionHintLabel = QLabel(self.portDetailsPanel)
|
||||
self.positionHintLabel.setObjectName(u"positionHintLabel")
|
||||
self.positionHintLabel.setWordWrap(True)
|
||||
|
||||
self.portDetailsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.positionHintLabel)
|
||||
self.portDetailsForm.setWidget(4, QFormLayout.ItemRole.SpanningRole, self.positionHintLabel)
|
||||
|
||||
self.portSplitter.addWidget(self.portDetailsPanel)
|
||||
|
||||
@@ -130,6 +136,7 @@ class Ui_PortOptionsDialog(object):
|
||||
self.orientationCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Input", None))
|
||||
self.orientationCombo.setItemText(1, QCoreApplication.translate("PortOptionsDialog", u"Output", 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))
|
||||
# retranslateUi
|
||||
|
||||
|
||||
167
BEdit/src/bedit/gui/generated/ui_text_definition_editor.py
Normal file
167
BEdit/src/bedit/gui/generated/ui_text_definition_editor.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'text_definition_editor.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.11.1
|
||||
##
|
||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
|
||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||
QMetaObject, QObject, QPoint, QRect,
|
||||
QSize, QTime, QUrl, Qt)
|
||||
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QAbstractItemView, QApplication, QGroupBox, QHBoxLayout,
|
||||
QHeaderView, QPlainTextEdit, QPushButton, QSizePolicy,
|
||||
QSpacerItem, QSplitter, QTableWidget, QTableWidgetItem,
|
||||
QVBoxLayout, QWidget)
|
||||
|
||||
class Ui_TextDefinitionEditor(object):
|
||||
def setupUi(self, TextDefinitionEditor):
|
||||
if not TextDefinitionEditor.objectName():
|
||||
TextDefinitionEditor.setObjectName(u"TextDefinitionEditor")
|
||||
TextDefinitionEditor.resize(900, 600)
|
||||
self.editorLayout = QHBoxLayout(TextDefinitionEditor)
|
||||
self.editorLayout.setObjectName(u"editorLayout")
|
||||
self.editorLayout.setContentsMargins(6, 6, 6, 6)
|
||||
self.columnSplitter = QSplitter(TextDefinitionEditor)
|
||||
self.columnSplitter.setObjectName(u"columnSplitter")
|
||||
self.columnSplitter.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.columnSplitter.setChildrenCollapsible(False)
|
||||
self.equationsGroup = QGroupBox(self.columnSplitter)
|
||||
self.equationsGroup.setObjectName(u"equationsGroup")
|
||||
self.equationsLayout = QVBoxLayout(self.equationsGroup)
|
||||
self.equationsLayout.setObjectName(u"equationsLayout")
|
||||
self.equationsEdit = QPlainTextEdit(self.equationsGroup)
|
||||
self.equationsEdit.setObjectName(u"equationsEdit")
|
||||
self.equationsEdit.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
||||
|
||||
self.equationsLayout.addWidget(self.equationsEdit)
|
||||
|
||||
self.columnSplitter.addWidget(self.equationsGroup)
|
||||
self.definitionSplitter = QSplitter(self.columnSplitter)
|
||||
self.definitionSplitter.setObjectName(u"definitionSplitter")
|
||||
self.definitionSplitter.setOrientation(Qt.Orientation.Vertical)
|
||||
self.definitionSplitter.setChildrenCollapsible(False)
|
||||
self.portsGroup = QGroupBox(self.definitionSplitter)
|
||||
self.portsGroup.setObjectName(u"portsGroup")
|
||||
self.portsLayout = QVBoxLayout(self.portsGroup)
|
||||
self.portsLayout.setObjectName(u"portsLayout")
|
||||
self.portsTable = QTableWidget(self.portsGroup)
|
||||
if (self.portsTable.columnCount() < 4):
|
||||
self.portsTable.setColumnCount(4)
|
||||
__qtablewidgetitem = QTableWidgetItem()
|
||||
self.portsTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
|
||||
__qtablewidgetitem1 = QTableWidgetItem()
|
||||
self.portsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
|
||||
__qtablewidgetitem2 = QTableWidgetItem()
|
||||
self.portsTable.setHorizontalHeaderItem(2, __qtablewidgetitem2)
|
||||
__qtablewidgetitem3 = QTableWidgetItem()
|
||||
self.portsTable.setHorizontalHeaderItem(3, __qtablewidgetitem3)
|
||||
self.portsTable.setObjectName(u"portsTable")
|
||||
self.portsTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.portsTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.portsTable.setColumnCount(4)
|
||||
|
||||
self.portsLayout.addWidget(self.portsTable)
|
||||
|
||||
self.portButtonsLayout = QHBoxLayout()
|
||||
self.portButtonsLayout.setObjectName(u"portButtonsLayout")
|
||||
self.addPortButton = QPushButton(self.portsGroup)
|
||||
self.addPortButton.setObjectName(u"addPortButton")
|
||||
|
||||
self.portButtonsLayout.addWidget(self.addPortButton)
|
||||
|
||||
self.removePortButton = QPushButton(self.portsGroup)
|
||||
self.removePortButton.setObjectName(u"removePortButton")
|
||||
|
||||
self.portButtonsLayout.addWidget(self.removePortButton)
|
||||
|
||||
self.portButtonSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||
|
||||
self.portButtonsLayout.addItem(self.portButtonSpacer)
|
||||
|
||||
|
||||
self.portsLayout.addLayout(self.portButtonsLayout)
|
||||
|
||||
self.definitionSplitter.addWidget(self.portsGroup)
|
||||
self.parametersGroup = QGroupBox(self.definitionSplitter)
|
||||
self.parametersGroup.setObjectName(u"parametersGroup")
|
||||
self.parametersLayout = QVBoxLayout(self.parametersGroup)
|
||||
self.parametersLayout.setObjectName(u"parametersLayout")
|
||||
self.parametersTable = QTableWidget(self.parametersGroup)
|
||||
if (self.parametersTable.columnCount() < 3):
|
||||
self.parametersTable.setColumnCount(3)
|
||||
__qtablewidgetitem4 = QTableWidgetItem()
|
||||
self.parametersTable.setHorizontalHeaderItem(0, __qtablewidgetitem4)
|
||||
__qtablewidgetitem5 = QTableWidgetItem()
|
||||
self.parametersTable.setHorizontalHeaderItem(1, __qtablewidgetitem5)
|
||||
__qtablewidgetitem6 = QTableWidgetItem()
|
||||
self.parametersTable.setHorizontalHeaderItem(2, __qtablewidgetitem6)
|
||||
self.parametersTable.setObjectName(u"parametersTable")
|
||||
self.parametersTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.parametersTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.parametersTable.setColumnCount(3)
|
||||
|
||||
self.parametersLayout.addWidget(self.parametersTable)
|
||||
|
||||
self.parameterButtonsLayout = QHBoxLayout()
|
||||
self.parameterButtonsLayout.setObjectName(u"parameterButtonsLayout")
|
||||
self.addParameterButton = QPushButton(self.parametersGroup)
|
||||
self.addParameterButton.setObjectName(u"addParameterButton")
|
||||
|
||||
self.parameterButtonsLayout.addWidget(self.addParameterButton)
|
||||
|
||||
self.removeParameterButton = QPushButton(self.parametersGroup)
|
||||
self.removeParameterButton.setObjectName(u"removeParameterButton")
|
||||
|
||||
self.parameterButtonsLayout.addWidget(self.removeParameterButton)
|
||||
|
||||
self.parameterButtonSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
|
||||
|
||||
self.parameterButtonsLayout.addItem(self.parameterButtonSpacer)
|
||||
|
||||
|
||||
self.parametersLayout.addLayout(self.parameterButtonsLayout)
|
||||
|
||||
self.definitionSplitter.addWidget(self.parametersGroup)
|
||||
self.columnSplitter.addWidget(self.definitionSplitter)
|
||||
|
||||
self.editorLayout.addWidget(self.columnSplitter)
|
||||
|
||||
|
||||
self.retranslateUi(TextDefinitionEditor)
|
||||
|
||||
QMetaObject.connectSlotsByName(TextDefinitionEditor)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, TextDefinitionEditor):
|
||||
self.equationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Equations", None))
|
||||
self.equationsEdit.setPlaceholderText(QCoreApplication.translate("TextDefinitionEditor", u"Enter equations here\u2026", None))
|
||||
self.portsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Ports", None))
|
||||
___qtablewidgetitem = self.portsTable.horizontalHeaderItem(0)
|
||||
___qtablewidgetitem.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))
|
||||
___qtablewidgetitem1 = self.portsTable.horizontalHeaderItem(1)
|
||||
___qtablewidgetitem1.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
|
||||
___qtablewidgetitem2 = self.portsTable.horizontalHeaderItem(2)
|
||||
___qtablewidgetitem2.setText(QCoreApplication.translate("TextDefinitionEditor", u"Orientation", None))
|
||||
___qtablewidgetitem3 = self.portsTable.horizontalHeaderItem(3)
|
||||
___qtablewidgetitem3.setText(QCoreApplication.translate("TextDefinitionEditor", u"Multiple", None))
|
||||
self.addPortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Port", None))
|
||||
self.removePortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Port", None))
|
||||
self.parametersGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Parameters", None))
|
||||
___qtablewidgetitem4 = self.parametersTable.horizontalHeaderItem(0)
|
||||
___qtablewidgetitem4.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))
|
||||
___qtablewidgetitem5 = self.parametersTable.horizontalHeaderItem(1)
|
||||
___qtablewidgetitem5.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
|
||||
___qtablewidgetitem6 = self.parametersTable.horizontalHeaderItem(2)
|
||||
___qtablewidgetitem6.setText(QCoreApplication.translate("TextDefinitionEditor", u"Value", None))
|
||||
self.addParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Parameter", None))
|
||||
self.removeParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Parameter", None))
|
||||
pass
|
||||
# retranslateUi
|
||||
|
||||
@@ -23,6 +23,7 @@ from PySide6.QtWidgets import (
|
||||
QGraphicsScene,
|
||||
QGraphicsSceneContextMenuEvent,
|
||||
QGraphicsSceneMouseEvent,
|
||||
QGraphicsSimpleTextItem,
|
||||
QGraphicsView,
|
||||
QApplication,
|
||||
QMenu,
|
||||
@@ -32,7 +33,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bedit.core.model import Annotation, Component, Connection, Endpoint, Port
|
||||
from bedit.core.model import Annotation, Component, Connection, Endpoint, Junction, Port
|
||||
from bedit.gui.controllers.document import DocumentController
|
||||
from bedit.gui.dialogs.connection_chooser import ConnectionChooserDialog
|
||||
from bedit.gui.models.library_tree import COMPONENT_MIME_TYPE
|
||||
@@ -78,6 +79,84 @@ class ConnectionPortItem(QGraphicsEllipseItem):
|
||||
self.setToolTip(label)
|
||||
|
||||
|
||||
class JunctionGraphicsItem(QGraphicsEllipseItem):
|
||||
def __init__(self, junction: Junction, controller: DocumentController) -> None:
|
||||
super().__init__(-5, -5, 10, 10)
|
||||
self.junction = junction
|
||||
self.controller = controller
|
||||
self.endpoint = Endpoint(junction=junction.id)
|
||||
self.drag_start = QPointF(junction.x, junction.y)
|
||||
self.setPos(junction.x, junction.y)
|
||||
self.setBrush(QColor("#303030"))
|
||||
self.setPen(QPen(QColor("#ffffff"), 1))
|
||||
self.setZValue(3)
|
||||
self.setToolTip("Connection junction")
|
||||
self.setCursor(Qt.CursorShape.SizeAllCursor)
|
||||
self.setFlags(
|
||||
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
|
||||
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
|
||||
)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
self.drag_start = self.pos()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
super().mouseReleaseEvent(event)
|
||||
self.controller.move_junction(self.junction.id, self.drag_start, self.pos())
|
||||
|
||||
def itemChange(self, change, value): # noqa: N802
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
|
||||
return _snapped(value)
|
||||
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.update_connections_for_junction(self.junction.id)
|
||||
return super().itemChange(change, value)
|
||||
|
||||
|
||||
class NameLabelItem(QGraphicsSimpleTextItem):
|
||||
"""Movable italic name label whose position is stored by its owner."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
owner_kind: str,
|
||||
owner_id: str,
|
||||
controller: DocumentController,
|
||||
parent: QGraphicsItem | None = None,
|
||||
) -> None:
|
||||
super().__init__(text, parent)
|
||||
self.owner_kind = owner_kind
|
||||
self.owner_id = owner_id
|
||||
self.controller = controller
|
||||
font = self.font()
|
||||
font.setItalic(True)
|
||||
self.setFont(font)
|
||||
self.setBrush(QColor("#303030"))
|
||||
self.setCursor(Qt.CursorShape.SizeAllCursor)
|
||||
self.drag_offset = QPointF()
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
parent = self.parentItem()
|
||||
point = parent.mapFromScene(event.scenePos()) if parent else event.scenePos()
|
||||
self.drag_offset = self.pos() - point
|
||||
event.accept()
|
||||
|
||||
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
parent = self.parentItem()
|
||||
point = parent.mapFromScene(event.scenePos()) if parent else event.scenePos()
|
||||
self.setPos(_snapped(point + self.drag_offset))
|
||||
event.accept()
|
||||
|
||||
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
|
||||
if self.owner_kind == "component":
|
||||
self.controller.move_component_name_label(self.owner_id, self.pos())
|
||||
else:
|
||||
self.controller.move_connection_name_label(self.owner_id, self.pos())
|
||||
event.accept()
|
||||
|
||||
|
||||
class ComponentGraphicsItem(QGraphicsObject):
|
||||
WIDTH = 128.0
|
||||
HEIGHT = 128.0
|
||||
@@ -98,6 +177,31 @@ class ComponentGraphicsItem(QGraphicsObject):
|
||||
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
|
||||
self.setTransformOriginPoint(self.hitbox.center())
|
||||
self.setRotation(component.rotation)
|
||||
self.name_label: NameLabelItem | None = None
|
||||
self.sync_name_label(component)
|
||||
|
||||
def sync_name_label(self, component: Component) -> None:
|
||||
if component.properties.get("showName", False):
|
||||
if self.name_label is None:
|
||||
self.name_label = NameLabelItem(
|
||||
component.name, "component", component.id, self.controller, self
|
||||
)
|
||||
self.name_label.setText(component.name)
|
||||
position = component.properties.get("nameLabelPosition")
|
||||
if isinstance(position, dict):
|
||||
self.name_label.setPos(float(position["x"]), float(position["y"]))
|
||||
else:
|
||||
bounds = self.name_label.boundingRect()
|
||||
self.name_label.setPos(
|
||||
self.hitbox.center().x() - bounds.width() / 2,
|
||||
self.hitbox.bottom() + 6,
|
||||
)
|
||||
self.name_label.setRotation(-component.rotation)
|
||||
elif self.name_label is not None:
|
||||
if self.name_label.scene() is not None:
|
||||
self.name_label.setParentItem(None)
|
||||
self.name_label.scene().removeItem(self.name_label)
|
||||
self.name_label = None
|
||||
|
||||
def _create_ports(self, ports, role: str, x: float) -> dict[str, ConnectionPortItem]:
|
||||
result = {}
|
||||
@@ -180,8 +284,8 @@ class ComponentGraphicsItem(QGraphicsObject):
|
||||
|
||||
|
||||
class InterfaceTerminalItem(QGraphicsObject):
|
||||
WIDTH = 110.0
|
||||
HEIGHT = 36.0
|
||||
WIDTH = 128.0
|
||||
HEIGHT = 32.0
|
||||
|
||||
def __init__(self, port: Port, direction: str, controller: DocumentController) -> None:
|
||||
super().__init__()
|
||||
@@ -255,13 +359,16 @@ class InterfaceTerminalItem(QGraphicsObject):
|
||||
class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
def __init__(
|
||||
self,
|
||||
connection_id: str,
|
||||
name: str = "",
|
||||
connection: Connection,
|
||||
controller: DocumentController,
|
||||
style: ConnectionStyle | None = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.connection_id = connection_id
|
||||
self.name = name
|
||||
self.connection_id = connection.id
|
||||
self.name = connection.name
|
||||
self.source_is_junction = connection.source.junction is not None
|
||||
self.target_is_junction = connection.target.junction is not None
|
||||
self.controller = controller
|
||||
self.style = style or ConnectionStyle()
|
||||
self.start = QPointF()
|
||||
self.end = QPointF()
|
||||
@@ -270,8 +377,34 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
|
||||
self._update_pen()
|
||||
self.setZValue(0)
|
||||
self.setToolTip(name or "Connection")
|
||||
self.setToolTip(connection.name or "Connection")
|
||||
self.handles: list[WaypointHandle] = []
|
||||
self.name_label: NameLabelItem | None = None
|
||||
self.sync_name_label(connection)
|
||||
|
||||
def sync_name_label(self, connection: Connection) -> None:
|
||||
visible = bool(connection.properties.get("showName", False))
|
||||
if not visible:
|
||||
if self.name_label is not None and self.name_label.scene() is not None:
|
||||
self.name_label.setParentItem(None)
|
||||
self.name_label.scene().removeItem(self.name_label)
|
||||
self.name_label = None
|
||||
return
|
||||
if self.name_label is None:
|
||||
self.name_label = NameLabelItem(
|
||||
connection.name, "connection", connection.id, self.controller, self
|
||||
)
|
||||
self.name_label.setText(connection.name)
|
||||
position = connection.properties.get("nameLabelPosition")
|
||||
if isinstance(position, dict):
|
||||
self.name_label.setPos(float(position["x"]), float(position["y"]))
|
||||
elif not self.path().isEmpty():
|
||||
bounds = self.path().boundingRect()
|
||||
label_bounds = self.name_label.boundingRect()
|
||||
self.name_label.setPos(
|
||||
bounds.center().x() - label_bounds.width() / 2,
|
||||
bounds.bottom() + 6,
|
||||
)
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
|
||||
if not self.isSelected():
|
||||
@@ -279,6 +412,7 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
self.setSelected(True)
|
||||
menu = QMenu()
|
||||
add_node_action = menu.addAction("Add Node")
|
||||
add_junction_action = menu.addAction("Add Junction")
|
||||
menu.addSeparator()
|
||||
options_action = menu.addAction("Connection Options…")
|
||||
selected = menu.exec(event.screenPos())
|
||||
@@ -286,6 +420,10 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.add_route_node("connection", self.connection_id, event.scenePos())
|
||||
elif selected is add_junction_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
scene.add_connection_junction(self.connection_id, event.scenePos())
|
||||
elif selected is options_action:
|
||||
scene = self.scene()
|
||||
if isinstance(scene, GraphScene):
|
||||
@@ -318,6 +456,10 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
if len(points) > 1:
|
||||
self.start_direction = points[1] - points[0]
|
||||
self.end_direction = points[-1] - points[-2]
|
||||
if self.name_label is not None:
|
||||
connection = self.controller.active_graph.connections.get(self.connection_id)
|
||||
if connection is not None:
|
||||
self.sync_name_label(connection)
|
||||
|
||||
def set_waypoints(self, points: list[QPointF]) -> None:
|
||||
for handle in self.handles:
|
||||
@@ -349,9 +491,9 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
||||
super().paint(painter, option, widget)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.setBrush(self.pen().color())
|
||||
if self.style.arrow_at_target:
|
||||
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))
|
||||
if self.style.arrow_at_source:
|
||||
if self.style.arrow_at_source and not self.source_is_junction:
|
||||
painter.drawPolygon(
|
||||
self._arrow(self.start, -self.start_direction, self.style.arrow_size)
|
||||
)
|
||||
@@ -700,8 +842,11 @@ class GraphScene(QGraphicsScene):
|
||||
self.input_items: dict[str, InterfaceTerminalItem] = {}
|
||||
self.output_items: dict[str, InterfaceTerminalItem] = {}
|
||||
self.connection_items: dict[str, ConnectionGraphicsItem] = {}
|
||||
self.junction_items: dict[str, JunctionGraphicsItem] = {}
|
||||
self.annotation_items: dict[str, QGraphicsItem] = {}
|
||||
self.pending_connection_item: ComponentGraphicsItem | ConnectionPortItem | None = None
|
||||
self.pending_connection_item: (
|
||||
ComponentGraphicsItem | ConnectionPortItem | JunctionGraphicsItem | None
|
||||
) = None
|
||||
self.pending_waypoints: list[QPointF] = []
|
||||
self.pending_preview: QGraphicsPathItem | None = None
|
||||
self.interaction_mode = "pointer"
|
||||
@@ -713,6 +858,7 @@ class GraphScene(QGraphicsScene):
|
||||
controller.activeGraphChanged.connect(self.rebuild)
|
||||
controller.componentMoved.connect(self.set_component_position)
|
||||
controller.componentRotated.connect(self.set_component_rotation)
|
||||
controller.componentPropertiesChanged.connect(self.refresh_component_properties)
|
||||
controller.graphItemChanged.connect(self.refresh_graph_item)
|
||||
self.rebuild()
|
||||
|
||||
@@ -722,6 +868,7 @@ class GraphScene(QGraphicsScene):
|
||||
self.input_items.clear()
|
||||
self.output_items.clear()
|
||||
self.connection_items.clear()
|
||||
self.junction_items.clear()
|
||||
self.annotation_items.clear()
|
||||
self.pending_connection_item = None
|
||||
self.pending_waypoints.clear()
|
||||
@@ -744,10 +891,14 @@ class GraphScene(QGraphicsScene):
|
||||
self.addItem(item)
|
||||
item.setPos(component.x, component.y)
|
||||
self.component_items[component.id] = item
|
||||
for junction in owner.graph.junctions.values():
|
||||
item = JunctionGraphicsItem(junction, self.controller)
|
||||
self.addItem(item)
|
||||
self.junction_items[junction.id] = item
|
||||
for connection in owner.graph.connections.values():
|
||||
item = ConnectionGraphicsItem(
|
||||
connection.id,
|
||||
connection.name,
|
||||
connection,
|
||||
self.controller,
|
||||
connection_style(self.controller.connection_port_type(connection)),
|
||||
)
|
||||
self.addItem(item)
|
||||
@@ -774,12 +925,33 @@ class GraphScene(QGraphicsScene):
|
||||
item = self.component_items.get(component_id)
|
||||
if item is not None:
|
||||
item.setRotation(rotation)
|
||||
if item.name_label is not None:
|
||||
item.name_label.setRotation(-rotation)
|
||||
self.update_connections_for_block(component_id)
|
||||
|
||||
def refresh_component_properties(self, component_id: str) -> None:
|
||||
component = (
|
||||
self.controller.document.find_component(component_id)
|
||||
if self.controller.document
|
||||
else None
|
||||
)
|
||||
item = self.component_items.get(component_id)
|
||||
if component is not None and item is not None:
|
||||
item.sync_name_label(component)
|
||||
|
||||
def refresh_graph_item(self, item_kind: str, item_id: str) -> None:
|
||||
if item_kind == "connection":
|
||||
if item_kind in {"connection", "connection_data"}:
|
||||
self.update_connection(item_id)
|
||||
return
|
||||
if item_kind == "junction_geometry":
|
||||
junction = self.controller.active_graph.junctions.get(item_id)
|
||||
graphics = self.junction_items.get(item_id)
|
||||
if junction is not None and graphics is not None:
|
||||
position = QPointF(junction.x, junction.y)
|
||||
if graphics.pos() != position:
|
||||
graphics.setPos(position)
|
||||
self.update_connections_for_junction(item_id)
|
||||
return
|
||||
annotation = self.controller.active_graph.annotations.get(item_id)
|
||||
graphics = self.annotation_items.get(item_id)
|
||||
if annotation is None or graphics is None:
|
||||
@@ -833,11 +1005,20 @@ class GraphScene(QGraphicsScene):
|
||||
if port_id in (connection.source.interface, connection.target.interface):
|
||||
self.update_connection(connection.id)
|
||||
|
||||
def update_connections_for_junction(self, junction_id: str) -> None:
|
||||
for connection in self.controller.active_graph.connections.values():
|
||||
if junction_id in (connection.source.junction, connection.target.junction):
|
||||
self.update_connection(connection.id)
|
||||
|
||||
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
|
||||
# The view paints the grid so it always covers the complete viewport.
|
||||
del painter, rect
|
||||
|
||||
def _endpoint_item(self, endpoint: Endpoint, role: str) -> ConnectionPortItem | None:
|
||||
def _endpoint_item(
|
||||
self, endpoint: Endpoint, role: str
|
||||
) -> ConnectionPortItem | JunctionGraphicsItem | None:
|
||||
if endpoint.junction is not None:
|
||||
return self.junction_items.get(endpoint.junction)
|
||||
if endpoint.interface is not None:
|
||||
terminals = self.input_items if role == "source" else self.output_items
|
||||
terminal = terminals.get(endpoint.interface)
|
||||
@@ -867,6 +1048,9 @@ class GraphScene(QGraphicsScene):
|
||||
if endpoint.block is not None:
|
||||
component = self.component_items.get(endpoint.block)
|
||||
return component.mapToScene(component.hitbox.center()) if component else None
|
||||
if endpoint.junction is not None:
|
||||
junction = self.junction_items.get(endpoint.junction)
|
||||
return junction.scenePos() if junction else None
|
||||
port = self._endpoint_item(endpoint, role)
|
||||
return port.scenePos() if port else None
|
||||
|
||||
@@ -876,6 +1060,9 @@ class GraphScene(QGraphicsScene):
|
||||
if endpoint.block is not None:
|
||||
component = self.component_items.get(endpoint.block)
|
||||
return self._hitbox_intersection(component, reference) if component else None
|
||||
if endpoint.junction is not None:
|
||||
junction = self.junction_items.get(endpoint.junction)
|
||||
return junction.scenePos() if junction else None
|
||||
port = self._endpoint_item(endpoint, role)
|
||||
return port.scenePos() if port else None
|
||||
|
||||
@@ -908,6 +1095,7 @@ class GraphScene(QGraphicsScene):
|
||||
start, end = geometry
|
||||
path, direction_points = self._route_path(start, end, waypoints)
|
||||
graphics.set_connection_path(path, direction_points)
|
||||
graphics.sync_name_label(connection)
|
||||
graphics.set_waypoints(waypoints)
|
||||
|
||||
def update_annotation(self, annotation_id: str) -> None:
|
||||
@@ -963,17 +1151,43 @@ class GraphScene(QGraphicsScene):
|
||||
return item.parentItem()
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _clicked_terminal(item: QGraphicsItem | None) -> InterfaceTerminalItem | None:
|
||||
if isinstance(item, InterfaceTerminalItem):
|
||||
return item
|
||||
if isinstance(item, ConnectionPortItem) and isinstance(
|
||||
item.parentItem(), InterfaceTerminalItem
|
||||
):
|
||||
return item.parentItem()
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _connection_click_item(
|
||||
cls, item: QGraphicsItem | None
|
||||
) -> ComponentGraphicsItem | ConnectionPortItem | JunctionGraphicsItem | None:
|
||||
if isinstance(item, JunctionGraphicsItem):
|
||||
return item
|
||||
component = cls._clicked_component(item)
|
||||
if component is not None:
|
||||
return item if isinstance(item, ConnectionPortItem) else component
|
||||
terminal = cls._clicked_terminal(item)
|
||||
return terminal.connection_port if terminal is not None else None
|
||||
|
||||
@staticmethod
|
||||
def _click_endpoint(item: QGraphicsItem | None) -> Endpoint | None:
|
||||
return item.endpoint if isinstance(item, ConnectionPortItem) else None
|
||||
return (
|
||||
item.endpoint
|
||||
if isinstance(item, (ConnectionPortItem, JunctionGraphicsItem))
|
||||
else None
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _connection_anchor(
|
||||
item: ComponentGraphicsItem | ConnectionPortItem,
|
||||
item: ComponentGraphicsItem | ConnectionPortItem | JunctionGraphicsItem,
|
||||
) -> QPointF:
|
||||
return (
|
||||
item.scenePos()
|
||||
if isinstance(item, ConnectionPortItem)
|
||||
if isinstance(item, (ConnectionPortItem, JunctionGraphicsItem))
|
||||
else item.sceneBoundingRect().center()
|
||||
)
|
||||
|
||||
@@ -991,10 +1205,16 @@ class GraphScene(QGraphicsScene):
|
||||
for input_port in target_item.component.inputs:
|
||||
if not PortTypeRegistry.compatible(output.type, input_port.type):
|
||||
continue
|
||||
source = Endpoint(block=source_item.component_id, port=output.id)
|
||||
target = Endpoint(block=target_item.component_id, port=input_port.id)
|
||||
if not self.controller.endpoint_accepts_connection(source, "source"):
|
||||
continue
|
||||
if not self.controller.endpoint_accepts_connection(target, "target"):
|
||||
continue
|
||||
choices.append(
|
||||
ConnectionChoice(
|
||||
Endpoint(block=source_item.component_id, port=output.id),
|
||||
Endpoint(block=target_item.component_id, port=input_port.id),
|
||||
source,
|
||||
target,
|
||||
f"{source_item.component.name}.{output.name} → "
|
||||
f"{target_item.component.name}.{input_port.name}",
|
||||
)
|
||||
@@ -1004,11 +1224,92 @@ class GraphScene(QGraphicsScene):
|
||||
add_pairs(second, first)
|
||||
return choices
|
||||
|
||||
def _interface_connection_choices(
|
||||
self,
|
||||
terminal: InterfaceTerminalItem,
|
||||
component: ComponentGraphicsItem,
|
||||
) -> list[ConnectionChoice]:
|
||||
choices: list[ConnectionChoice] = []
|
||||
interface = Endpoint(interface=terminal.port.id)
|
||||
if terminal.direction == "input":
|
||||
for port in component.component.inputs:
|
||||
target = Endpoint(block=component.component_id, port=port.id)
|
||||
if not PortTypeRegistry.compatible(terminal.port.type, port.type):
|
||||
continue
|
||||
if not self.controller.endpoint_accepts_connection(interface, "source"):
|
||||
continue
|
||||
if not self.controller.endpoint_accepts_connection(target, "target"):
|
||||
continue
|
||||
choices.append(
|
||||
ConnectionChoice(
|
||||
interface,
|
||||
target,
|
||||
f"IN.{terminal.port.name} → {component.component.name}.{port.name}",
|
||||
)
|
||||
)
|
||||
else:
|
||||
for port in component.component.outputs:
|
||||
source = Endpoint(block=component.component_id, port=port.id)
|
||||
if not PortTypeRegistry.compatible(port.type, terminal.port.type):
|
||||
continue
|
||||
if not self.controller.endpoint_accepts_connection(source, "source"):
|
||||
continue
|
||||
if not self.controller.endpoint_accepts_connection(interface, "target"):
|
||||
continue
|
||||
choices.append(
|
||||
ConnectionChoice(
|
||||
source,
|
||||
interface,
|
||||
f"{component.component.name}.{port.name} → OUT.{terminal.port.name}",
|
||||
)
|
||||
)
|
||||
return choices
|
||||
|
||||
def _junction_connection_choices(
|
||||
self,
|
||||
junction: JunctionGraphicsItem,
|
||||
component: ComponentGraphicsItem,
|
||||
) -> list[ConnectionChoice]:
|
||||
choices: list[ConnectionChoice] = []
|
||||
source = junction.endpoint
|
||||
for port in component.component.inputs:
|
||||
target = Endpoint(block=component.component_id, port=port.id)
|
||||
if not PortTypeRegistry.compatible(junction.junction.type, port.type):
|
||||
continue
|
||||
if not self.controller.endpoint_accepts_connection(target, "target"):
|
||||
continue
|
||||
choices.append(
|
||||
ConnectionChoice(
|
||||
source,
|
||||
target,
|
||||
f"Junction → {component.component.name}.{port.name}",
|
||||
)
|
||||
)
|
||||
return choices
|
||||
|
||||
def _junction_interface_choices(
|
||||
self,
|
||||
junction: JunctionGraphicsItem,
|
||||
terminal: InterfaceTerminalItem,
|
||||
) -> list[ConnectionChoice]:
|
||||
if terminal.direction != "output":
|
||||
return []
|
||||
target = Endpoint(interface=terminal.port.id)
|
||||
if not PortTypeRegistry.compatible(junction.junction.type, terminal.port.type):
|
||||
return []
|
||||
if not self.controller.endpoint_accepts_connection(target, "target"):
|
||||
return []
|
||||
return [
|
||||
ConnectionChoice(
|
||||
junction.endpoint,
|
||||
target,
|
||||
f"Junction → OUT.{terminal.port.name}",
|
||||
)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _default_choice_index(
|
||||
choices: list[ConnectionChoice],
|
||||
first_component: ComponentGraphicsItem,
|
||||
second_component: ComponentGraphicsItem,
|
||||
first_endpoint: Endpoint | None,
|
||||
second_endpoint: Endpoint | None,
|
||||
) -> int:
|
||||
@@ -1018,10 +1319,6 @@ class GraphScene(QGraphicsScene):
|
||||
value += 8
|
||||
if second_endpoint is not None and second_endpoint in (choice.source, choice.target):
|
||||
value += 8
|
||||
if choice.source.block == first_component.component_id:
|
||||
value += 2
|
||||
if choice.target.block == second_component.component_id:
|
||||
value += 1
|
||||
return value
|
||||
|
||||
return max(range(len(choices)), key=lambda index: score(choices[index]))
|
||||
@@ -1035,27 +1332,41 @@ class GraphScene(QGraphicsScene):
|
||||
return
|
||||
first_component = self._clicked_component(first_item)
|
||||
second_component = self._clicked_component(second_item)
|
||||
if (
|
||||
first_component is None
|
||||
or second_component is None
|
||||
or first_component is second_component
|
||||
):
|
||||
first_terminal = self._clicked_terminal(first_item)
|
||||
second_terminal = self._clicked_terminal(second_item)
|
||||
first_junction = first_item if isinstance(first_item, JunctionGraphicsItem) else None
|
||||
second_junction = second_item if isinstance(second_item, JunctionGraphicsItem) else None
|
||||
if first_component is not None and second_component is not None:
|
||||
if first_component is second_component:
|
||||
self._clear_pending_connection()
|
||||
return
|
||||
choices = self._connection_choices(first_component, second_component)
|
||||
elif first_terminal is not None and second_component is not None:
|
||||
choices = self._interface_connection_choices(first_terminal, second_component)
|
||||
elif first_component is not None and second_terminal is not None:
|
||||
choices = self._interface_connection_choices(second_terminal, first_component)
|
||||
elif first_junction is not None and second_component is not None:
|
||||
choices = self._junction_connection_choices(first_junction, second_component)
|
||||
elif first_component is not None and second_junction is not None:
|
||||
choices = self._junction_connection_choices(second_junction, first_component)
|
||||
elif first_junction is not None and second_terminal is not None:
|
||||
choices = self._junction_interface_choices(first_junction, second_terminal)
|
||||
elif first_terminal is not None and second_junction is not None:
|
||||
choices = self._junction_interface_choices(second_junction, first_terminal)
|
||||
else:
|
||||
self._clear_pending_connection()
|
||||
return
|
||||
if not choices:
|
||||
QToolTip.showText(
|
||||
self.views()[0].mapToGlobal(self.views()[0].viewport().rect().center())
|
||||
if self.views()
|
||||
else QPointF().toPoint(),
|
||||
"These blocks have no compatible input/output pairs",
|
||||
"These items have no available compatible input/output pairs",
|
||||
)
|
||||
self._clear_pending_connection()
|
||||
return
|
||||
default = self._default_choice_index(
|
||||
choices,
|
||||
first_component,
|
||||
second_component,
|
||||
self._click_endpoint(first_item),
|
||||
self._click_endpoint(second_item),
|
||||
)
|
||||
@@ -1087,9 +1398,8 @@ class GraphScene(QGraphicsScene):
|
||||
event.accept()
|
||||
return
|
||||
item = self.itemAt(event.scenePos(), QTransform())
|
||||
component = self._clicked_component(item)
|
||||
if component is not None:
|
||||
clicked = item if isinstance(item, ConnectionPortItem) else component
|
||||
clicked = self._connection_click_item(item)
|
||||
if clicked is not None:
|
||||
if self.pending_connection_item is None:
|
||||
self.pending_connection_item = clicked
|
||||
self.pending_waypoints = []
|
||||
@@ -1238,6 +1548,44 @@ class GraphScene(QGraphicsScene):
|
||||
points.insert(insertion, snapped)
|
||||
self.controller.set_route_waypoints(item_kind, item_id, points)
|
||||
|
||||
def add_connection_junction(self, connection_id: str, position: QPointF) -> None:
|
||||
connection, points = self._route_values("connection", connection_id)
|
||||
graphics = self.connection_items.get(connection_id)
|
||||
if connection is None or graphics is None:
|
||||
return
|
||||
snapped = _snapped(position)
|
||||
anchors = [graphics.start, *points, graphics.end]
|
||||
|
||||
def segment_distance(point: QPointF, first: QPointF, second: QPointF) -> float:
|
||||
delta = second - first
|
||||
length_squared = delta.x() ** 2 + delta.y() ** 2
|
||||
if length_squared == 0:
|
||||
return (point - first).manhattanLength()
|
||||
ratio = max(
|
||||
0.0,
|
||||
min(
|
||||
1.0,
|
||||
((point.x() - first.x()) * delta.x()
|
||||
+ (point.y() - first.y()) * delta.y())
|
||||
/ length_squared,
|
||||
),
|
||||
)
|
||||
nearest = first + delta * ratio
|
||||
return (point.x() - nearest.x()) ** 2 + (point.y() - nearest.y()) ** 2
|
||||
|
||||
insertion = min(
|
||||
range(len(anchors) - 1),
|
||||
key=lambda index: segment_distance(
|
||||
snapped, anchors[index], anchors[index + 1]
|
||||
),
|
||||
)
|
||||
self.controller.split_connection(
|
||||
connection_id,
|
||||
snapped,
|
||||
points[:insertion],
|
||||
points[insertion:],
|
||||
)
|
||||
|
||||
def move_route_node(self, item_kind: str, item_id: str, index: int, position: QPointF) -> None:
|
||||
item, points = self._route_values(item_kind, item_id)
|
||||
if item is None:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
@@ -13,7 +12,7 @@ from PySide6.QtWidgets import (
|
||||
QTabWidget,
|
||||
)
|
||||
|
||||
from bedit.core.model import Component, Port
|
||||
from bedit.core.model import Component, Parameter
|
||||
from bedit.core.serializer import JsonDocumentSerializer
|
||||
from bedit.gui.controllers.document import DocumentController
|
||||
from bedit.gui.dialogs.component_options import ComponentOptionsDialog
|
||||
@@ -40,6 +39,7 @@ class MainWindow(QMainWindow):
|
||||
self.ui = Ui_MainWindow()
|
||||
self.ui.setupUi(self)
|
||||
self.settings = application_settings()
|
||||
self._applying_text_definition = False
|
||||
|
||||
self.libraries = LibraryRepository(self)
|
||||
self.document_controller = DocumentController(self)
|
||||
@@ -108,12 +108,18 @@ class MainWindow(QMainWindow):
|
||||
self.ui.lineToolButton.clicked.connect(lambda: self.set_graph_tool("line"))
|
||||
self.ui.textToolButton.clicked.connect(lambda: self.set_graph_tool("text"))
|
||||
self.ui.rotateToolButton.clicked.connect(self.ui.graphView.rotate_selected)
|
||||
self.ui.applyJsonButton.clicked.connect(self.apply_json)
|
||||
self.ui.textDefinitionEditor.definitionEdited.connect(
|
||||
self.apply_text_definition
|
||||
)
|
||||
self.document_controller.activeGraphChanged.connect(self._active_graph_changed)
|
||||
self.document_controller.textDefinitionChanged.connect(
|
||||
self._text_definition_changed
|
||||
)
|
||||
|
||||
def _connect_actions(self) -> None:
|
||||
self.ui.actionNew.triggered.connect(self.new_document)
|
||||
self.ui.actionOpen.triggered.connect(self.open_document)
|
||||
self.ui.actionReloadLibraries.triggered.connect(self.reload_libraries)
|
||||
self.ui.actionSave.triggered.connect(self.save_document)
|
||||
self.ui.actionSaveAs.triggered.connect(self.save_document_as)
|
||||
self.ui.actionClose.triggered.connect(self.close_document)
|
||||
@@ -131,6 +137,7 @@ class MainWindow(QMainWindow):
|
||||
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection)
|
||||
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
|
||||
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
|
||||
self.addAction(self.ui.actionRotateClockwise)
|
||||
self.ui.actionZoomIn.triggered.connect(self.ui.graphView.zoom_in)
|
||||
self.ui.actionZoomOut.triggered.connect(self.ui.graphView.zoom_out)
|
||||
self.ui.actionCenterView.triggered.connect(self.ui.graphView.center_workspace)
|
||||
@@ -188,7 +195,6 @@ class MainWindow(QMainWindow):
|
||||
self.ui.navigateUpButton.setEnabled(False)
|
||||
self.ui.workspaceModeLabel.setText("")
|
||||
self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage)
|
||||
self.ui.applyJsonButton.setVisible(False)
|
||||
self._set_graph_controls_visible(False)
|
||||
self._update_edit_actions()
|
||||
return
|
||||
@@ -198,13 +204,14 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
is_graph = component.implementation_kind == "graph"
|
||||
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
|
||||
self.ui.workspaceStack.setCurrentWidget(self.ui.graphPage if is_graph else self.ui.jsonPage)
|
||||
self.ui.applyJsonButton.setVisible(not is_graph)
|
||||
self.ui.workspaceStack.setCurrentWidget(
|
||||
self.ui.graphPage if is_graph else self.ui.textPage
|
||||
)
|
||||
self._set_graph_controls_visible(is_graph)
|
||||
if is_graph:
|
||||
self.set_graph_tool("pointer")
|
||||
else:
|
||||
self._load_source_json()
|
||||
self._load_text_definition()
|
||||
self._update_edit_actions()
|
||||
|
||||
def _update_edit_actions(self) -> None:
|
||||
@@ -258,62 +265,71 @@ class MainWindow(QMainWindow):
|
||||
"text": self.ui.textToolButton,
|
||||
}[mode].setChecked(True)
|
||||
|
||||
def _load_source_json(self) -> None:
|
||||
def _load_text_definition(self) -> None:
|
||||
component = self.document_controller.active_component
|
||||
if component is None:
|
||||
return
|
||||
text = json.dumps(
|
||||
{
|
||||
"inputs": [port.to_dict() for port in component.inputs],
|
||||
"outputs": [port.to_dict() for port in component.outputs],
|
||||
"source": component.source,
|
||||
},
|
||||
indent=2,
|
||||
self.ui.textDefinitionEditor.set_definition(
|
||||
component.source.get("equations", ""),
|
||||
component.inputs,
|
||||
component.outputs,
|
||||
[
|
||||
Parameter.from_dict(parameter)
|
||||
for parameter in component.source.get("parameters", [])
|
||||
],
|
||||
)
|
||||
self.ui.jsonEditor.setPlainText(text)
|
||||
self.ui.jsonEditor.document().setModified(False)
|
||||
|
||||
def _resolve_source_edits(self) -> bool:
|
||||
component = self.document_controller.active_component
|
||||
if (
|
||||
component is None
|
||||
or component.implementation_kind != "text"
|
||||
or not self.ui.jsonEditor.document().isModified()
|
||||
or not self.ui.textDefinitionEditor.is_modified
|
||||
):
|
||||
return True
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"Apply text component changes?",
|
||||
"The text component has unapplied input, output, or source changes.",
|
||||
"The text component has unapplied equation, port, or parameter changes.",
|
||||
QMessageBox.StandardButton.Apply
|
||||
| QMessageBox.StandardButton.Discard
|
||||
| QMessageBox.StandardButton.Cancel,
|
||||
)
|
||||
if answer == QMessageBox.StandardButton.Apply:
|
||||
return self.apply_json()
|
||||
return self.apply_text_definition()
|
||||
return answer == QMessageBox.StandardButton.Discard
|
||||
|
||||
@Slot()
|
||||
def apply_json(self) -> bool:
|
||||
def apply_text_definition(self) -> bool:
|
||||
try:
|
||||
data = json.loads(self.ui.jsonEditor.toPlainText())
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("The text component JSON must be an object")
|
||||
if not isinstance(data.get("inputs"), list):
|
||||
raise ValueError("'inputs' must be a list")
|
||||
if not isinstance(data.get("outputs"), list):
|
||||
raise ValueError("'outputs' must be a list")
|
||||
if not isinstance(data.get("source"), dict):
|
||||
raise ValueError("'source' must be an object")
|
||||
inputs = [Port.from_dict(item) for item in data["inputs"]]
|
||||
outputs = [Port.from_dict(item) for item in data["outputs"]]
|
||||
self.document_controller.replace_active_text_definition(inputs, outputs, data["source"])
|
||||
except (TypeError, ValueError, json.JSONDecodeError) as error:
|
||||
QMessageBox.critical(self, "Invalid text component JSON", str(error))
|
||||
inputs, outputs = self.ui.textDefinitionEditor.ports
|
||||
self._applying_text_definition = True
|
||||
try:
|
||||
self.document_controller.replace_active_text_definition(
|
||||
inputs,
|
||||
outputs,
|
||||
self.ui.textDefinitionEditor.equations,
|
||||
self.ui.textDefinitionEditor.parameters,
|
||||
)
|
||||
finally:
|
||||
self._applying_text_definition = False
|
||||
except (TypeError, ValueError) as error:
|
||||
QMessageBox.critical(self, "Invalid text component", str(error))
|
||||
self._load_text_definition()
|
||||
return False
|
||||
self._load_source_json()
|
||||
self.ui.textDefinitionEditor.set_modified(False)
|
||||
return True
|
||||
|
||||
@Slot(str)
|
||||
def _text_definition_changed(self, component_id: str) -> None:
|
||||
component = self.document_controller.active_component
|
||||
if (
|
||||
component is not None
|
||||
and component.id == component_id
|
||||
and not self._applying_text_definition
|
||||
):
|
||||
self._load_text_definition()
|
||||
|
||||
def _maybe_save(self) -> bool:
|
||||
if self.document_controller.document is None:
|
||||
return True
|
||||
@@ -435,9 +451,9 @@ class MainWindow(QMainWindow):
|
||||
ports_action = menu.addAction("Port Options…")
|
||||
delete_action = menu.addAction("Delete")
|
||||
selected = menu.exec(tree_view.viewport().mapToGlobal(position))
|
||||
if selected is graph_action:
|
||||
if graph_action is not None and selected is graph_action:
|
||||
self.document_controller.add_child(component_id, "graph")
|
||||
elif selected is text_action:
|
||||
elif text_action is not None and selected is text_action:
|
||||
self.document_controller.add_child(component_id, "text")
|
||||
elif selected is options_action:
|
||||
self.show_component_options(component_id)
|
||||
@@ -520,6 +536,7 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
dialog = ComponentOptionsDialog(component, self)
|
||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||
try:
|
||||
self.document_controller.edit_component_appearance(
|
||||
component_id,
|
||||
dialog.ui.nameEdit.text().strip(),
|
||||
@@ -527,7 +544,10 @@ class MainWindow(QMainWindow):
|
||||
dialog.edited_inputs,
|
||||
dialog.edited_outputs,
|
||||
dialog.ui.showSubtreeCheckBox.isChecked(),
|
||||
dialog.ui.showNameCheckBox.isChecked(),
|
||||
)
|
||||
except ValueError as error:
|
||||
QMessageBox.warning(self, "Cannot rename component", str(error))
|
||||
|
||||
@Slot(str, str)
|
||||
def show_port_options(self, port_id: str, direction: str) -> None:
|
||||
@@ -550,9 +570,17 @@ class MainWindow(QMainWindow):
|
||||
connection = owner.graph.connections.get(connection_id)
|
||||
if connection is None:
|
||||
return
|
||||
dialog = ItemOptionsDialog("Connection Options", connection.name, self, name_required=False)
|
||||
dialog = ItemOptionsDialog(
|
||||
"Connection Options",
|
||||
connection.name,
|
||||
self,
|
||||
name_required=False,
|
||||
show_name=bool(connection.properties.get("showName", False)),
|
||||
)
|
||||
if dialog.exec() == dialog.DialogCode.Accepted:
|
||||
self.document_controller.rename_connection(connection_id, dialog.name)
|
||||
self.document_controller.edit_connection_options(
|
||||
connection_id, dialog.name, dialog.show_name
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def show_about(self) -> None:
|
||||
|
||||
@@ -86,7 +86,9 @@ class DocumentTreeModel(LibraryTreeModel):
|
||||
self.clear()
|
||||
self.setHorizontalHeaderLabels(["Document"])
|
||||
if self.controller.document is not None:
|
||||
current_root = QStandardItem("Current Document")
|
||||
current_root = QStandardItem(
|
||||
str(self.controller.document.metadata.get("name") or "Current Document")
|
||||
)
|
||||
current_root.setDragEnabled(False)
|
||||
current_root.setData("current-document", ITEM_KIND_ROLE)
|
||||
for component in self.controller.document.roots.values():
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<item row="1" column="0"><widget class="QLabel" name="iconLabel"><property name="text"><string>Icon:</string></property></widget></item>
|
||||
<item row="1" column="1"><widget class="QPushButton" name="editIconButton"><property name="text"><string>Edit Icon…</string></property><property name="toolTip"><string>Open the vector icon and port-position editor</string></property></widget></item>
|
||||
<item row="2" column="0" colspan="2"><widget class="QCheckBox" name="showSubtreeCheckBox"><property name="text"><string>Show contained components in the Libraries tree</string></property><property name="checked"><bool>true</bool></property></widget></item>
|
||||
<item row="3" column="0" colspan="2"><widget class="QCheckBox" name="showNameCheckBox"><property name="text"><string>Show name below component</string></property></widget></item>
|
||||
</layout>
|
||||
</item>
|
||||
<item><spacer name="optionsSpacer"><property name="orientation"><enum>Qt::Orientation::Vertical</enum></property><property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property></spacer></item>
|
||||
|
||||
@@ -264,16 +264,6 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="applyJsonButton">
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Apply JSON</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="pointerToolButton">
|
||||
<property name="text">
|
||||
@@ -397,8 +387,8 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="jsonPage">
|
||||
<layout class="QVBoxLayout" name="jsonPageLayout">
|
||||
<widget class="QWidget" name="textPage">
|
||||
<layout class="QVBoxLayout" name="textPageLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
@@ -412,14 +402,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="jsonEditor">
|
||||
<property name="lineWrapMode">
|
||||
<enum>QPlainTextEdit::LineWrapMode::NoWrap</enum>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Component JSON</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="TextDefinitionEditor" name="textDefinitionEditor" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
@@ -466,6 +449,7 @@
|
||||
</property>
|
||||
<addaction name="actionNew"/>
|
||||
<addaction name="actionOpen"/>
|
||||
<addaction name="actionReloadLibraries"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionSave"/>
|
||||
<addaction name="actionSaveAs"/>
|
||||
@@ -667,6 +651,17 @@
|
||||
<string>Ctrl+O</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionReloadLibraries">
|
||||
<property name="text">
|
||||
<string>Reload &Libraries</string>
|
||||
</property>
|
||||
<property name="statusTip">
|
||||
<string>Reload configured library files from disk</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F5</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSave">
|
||||
<property name="icon">
|
||||
<iconset resource="../resources/resources.qrc">
|
||||
@@ -806,6 +801,12 @@
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>TextDefinitionEditor</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bedit.gui.editors.text_definition</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>GraphWorkspaceView</class>
|
||||
<extends>QGraphicsView</extends>
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
<item row="1" column="1"><widget class="QComboBox" name="typeCombo"><item><property name="text"><string>Signal</string></property></item></widget></item>
|
||||
<item row="2" column="0"><widget class="QLabel" name="orientationLabel"><property name="text"><string>Orientation:</string></property></widget></item>
|
||||
<item row="2" column="1"><widget class="QComboBox" name="orientationCombo"><item><property name="text"><string>Input</string></property></item><item><property name="text"><string>Output</string></property></item></widget></item>
|
||||
<item row="3" column="0" colspan="2"><widget class="QLabel" name="positionHintLabel"><property name="text"><string>New ports start at (0, 0) in the icon editor.</string></property><property name="wordWrap"><bool>true</bool></property></widget></item>
|
||||
<item row="3" column="0" colspan="2"><widget class="QCheckBox" name="multipleConnectionsCheckBox"><property name="text"><string>Allow multiple connections</string></property></widget></item>
|
||||
<item row="4" column="0" colspan="2"><widget class="QLabel" name="positionHintLabel"><property name="text"><string>New ports start at (0, 0) in the icon editor.</string></property><property name="wordWrap"><bool>true</bool></property></widget></item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
|
||||
45
BEdit/ui/text_definition_editor.ui
Normal file
45
BEdit/ui/text_definition_editor.ui
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TextDefinitionEditor</class>
|
||||
<widget class="QWidget" name="TextDefinitionEditor">
|
||||
<property name="geometry"><rect><x>0</x><y>0</y><width>900</width><height>600</height></rect></property>
|
||||
<layout class="QHBoxLayout" name="editorLayout">
|
||||
<property name="leftMargin"><number>6</number></property>
|
||||
<property name="topMargin"><number>6</number></property>
|
||||
<property name="rightMargin"><number>6</number></property>
|
||||
<property name="bottomMargin"><number>6</number></property>
|
||||
<item>
|
||||
<widget class="QSplitter" name="columnSplitter">
|
||||
<property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
|
||||
<property name="childrenCollapsible"><bool>false</bool></property>
|
||||
<widget class="QGroupBox" name="equationsGroup">
|
||||
<property name="title"><string>Equations</string></property>
|
||||
<layout class="QVBoxLayout" name="equationsLayout">
|
||||
<item><widget class="QPlainTextEdit" name="equationsEdit"><property name="lineWrapMode"><enum>QPlainTextEdit::LineWrapMode::NoWrap</enum></property><property name="placeholderText"><string>Enter equations here…</string></property></widget></item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QSplitter" name="definitionSplitter">
|
||||
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
|
||||
<property name="childrenCollapsible"><bool>false</bool></property>
|
||||
<widget class="QGroupBox" name="portsGroup">
|
||||
<property name="title"><string>Ports</string></property>
|
||||
<layout class="QVBoxLayout" name="portsLayout">
|
||||
<item><widget class="QTableWidget" name="portsTable"><property name="selectionBehavior"><enum>QAbstractItemView::SelectionBehavior::SelectRows</enum></property><property name="selectionMode"><enum>QAbstractItemView::SelectionMode::SingleSelection</enum></property><property name="columnCount"><number>4</number></property><column><property name="text"><string>Name</string></property></column><column><property name="text"><string>Type</string></property></column><column><property name="text"><string>Orientation</string></property></column><column><property name="text"><string>Multiple</string></property></column></widget></item>
|
||||
<item><layout class="QHBoxLayout" name="portButtonsLayout"><item><widget class="QPushButton" name="addPortButton"><property name="text"><string>Add Port</string></property></widget></item><item><widget class="QPushButton" name="removePortButton"><property name="text"><string>Remove Port</string></property></widget></item><item><spacer name="portButtonSpacer"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item></layout></item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="parametersGroup">
|
||||
<property name="title"><string>Parameters</string></property>
|
||||
<layout class="QVBoxLayout" name="parametersLayout">
|
||||
<item><widget class="QTableWidget" name="parametersTable"><property name="selectionBehavior"><enum>QAbstractItemView::SelectionBehavior::SelectRows</enum></property><property name="selectionMode"><enum>QAbstractItemView::SelectionMode::SingleSelection</enum></property><property name="columnCount"><number>3</number></property><column><property name="text"><string>Name</string></property></column><column><property name="text"><string>Type</string></property></column><column><property name="text"><string>Value</string></property></column></widget></item>
|
||||
<item><layout class="QHBoxLayout" name="parameterButtonsLayout"><item><widget class="QPushButton" name="addParameterButton"><property name="text"><string>Add Parameter</string></property></widget></item><item><widget class="QPushButton" name="removeParameterButton"><property name="text"><string>Remove Parameter</string></property></widget></item><item><spacer name="parameterButtonSpacer"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item></layout></item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -6,16 +6,50 @@
|
||||
},
|
||||
"roots": [
|
||||
{
|
||||
"id": "f4a9c769-d49a-46e1-90d8-a8175bf42e9c",
|
||||
"name": "New Graph Block 1",
|
||||
"id": "57fe8127-ea9d-4a8b-8d09-da430ffbbd92",
|
||||
"name": "PID",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [],
|
||||
"outputs": []
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-1342716d",
|
||||
"name": "in",
|
||||
"position": {
|
||||
"x": -384.0,
|
||||
"y": -112.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-c75d0f16",
|
||||
"name": "out",
|
||||
"position": {
|
||||
"x": 320.0,
|
||||
"y": -112.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
@@ -31,7 +65,7 @@
|
||||
"type": "rectangle",
|
||||
"x": 32.0,
|
||||
"y": 32.0,
|
||||
"width": 88.0,
|
||||
"width": 64.0,
|
||||
"height": 64.0,
|
||||
"fill": "#f4f4f4",
|
||||
"stroke": "#303030",
|
||||
@@ -43,63 +77,67 @@
|
||||
"type": "text",
|
||||
"x": 40.0,
|
||||
"y": 40.0,
|
||||
"width": 72.0,
|
||||
"height": 40.0,
|
||||
"width": 48.0,
|
||||
"height": 48.0,
|
||||
"text": "Graph",
|
||||
"color": "#202020",
|
||||
"fontSize": 12.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {},
|
||||
"properties": {
|
||||
"showName": false
|
||||
},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
"showSubtree": false
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "graph",
|
||||
"graph": {
|
||||
"blocks": [
|
||||
{
|
||||
"id": "af73487a-4a2d-4908-b919-d0a2eaa75ce2",
|
||||
"name": "A",
|
||||
"id": "cbfb5d1f-38cc-412e-b8e3-53aa82e46eb8",
|
||||
"name": "g_D",
|
||||
"position": {
|
||||
"x": -224.0,
|
||||
"y": -168.0
|
||||
"x": -128.0,
|
||||
"y": -160.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-5c5d4695",
|
||||
"name": "Port 1",
|
||||
"id": "port-df34ce84",
|
||||
"name": "in",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 32.0,
|
||||
"x": 64.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal"
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-de109124",
|
||||
"name": "Port 2",
|
||||
"id": "port-fe9e6486",
|
||||
"name": "out",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 96.0,
|
||||
"y": 64.0
|
||||
"x": 88.0,
|
||||
"y": 40.0
|
||||
}
|
||||
},
|
||||
"type": "signal"
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -126,14 +164,14 @@
|
||||
"y": 32.0
|
||||
},
|
||||
{
|
||||
"color": "#303030",
|
||||
"color": "#00007f",
|
||||
"fill": "#ffffff",
|
||||
"fontSize": 12.0,
|
||||
"fontSize": 24.0,
|
||||
"height": 48.0,
|
||||
"lineStyle": "solid",
|
||||
"lineWidth": 1.5,
|
||||
"stroke": "#303030",
|
||||
"text": "A",
|
||||
"stroke": "#00007f",
|
||||
"text": "K",
|
||||
"type": "text",
|
||||
"width": 48.0,
|
||||
"x": 40.0,
|
||||
@@ -141,74 +179,70 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {},
|
||||
"properties": {
|
||||
"showName": true
|
||||
},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": [],
|
||||
"parameters": {}
|
||||
"equations": "out = k*in;",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "parameter-61a43a86",
|
||||
"name": "k",
|
||||
"type": "real",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b89a77a1-d773-4ee3-8c3f-74377c29b4b2",
|
||||
"name": "B",
|
||||
"id": "478e7ff8-baa1-467f-8707-e375b99395f6",
|
||||
"name": "differentiate0",
|
||||
"position": {
|
||||
"x": 96.0,
|
||||
"y": -288.0
|
||||
"x": 0.0,
|
||||
"y": -160.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-4f732b3e",
|
||||
"name": "Port 1",
|
||||
"id": "port-1cbabc8f",
|
||||
"name": "in",
|
||||
"position": {
|
||||
"x": -176.0,
|
||||
"y": -144.0
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 32.0,
|
||||
"y": 48.0
|
||||
"x": 64.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal"
|
||||
},
|
||||
{
|
||||
"id": "port-ef7c4218",
|
||||
"name": "Port 2",
|
||||
"position": {
|
||||
"x": -176.0,
|
||||
"y": -16.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 32.0,
|
||||
"y": 80.0
|
||||
}
|
||||
},
|
||||
"type": "signal"
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-b68679b2",
|
||||
"name": "Port 3",
|
||||
"id": "port-4eeea4e7",
|
||||
"name": "out",
|
||||
"position": {
|
||||
"x": 128.0,
|
||||
"y": -80.0
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 96.0,
|
||||
"y": 64.0
|
||||
"x": 88.0,
|
||||
"y": 40.0
|
||||
}
|
||||
},
|
||||
"type": "signal"
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -216,7 +250,7 @@
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "Graph",
|
||||
"text": "Text",
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 128.0
|
||||
@@ -235,14 +269,14 @@
|
||||
"y": 32.0
|
||||
},
|
||||
{
|
||||
"color": "#303030",
|
||||
"color": "#00007f",
|
||||
"fill": "#ffffff",
|
||||
"fontSize": 12.0,
|
||||
"fontSize": 18.0,
|
||||
"height": 48.0,
|
||||
"lineStyle": "solid",
|
||||
"lineWidth": 1.5,
|
||||
"stroke": "#303030",
|
||||
"text": "B",
|
||||
"stroke": "#00007f",
|
||||
"text": "d/dt",
|
||||
"type": "text",
|
||||
"width": 48.0,
|
||||
"x": 40.0,
|
||||
@@ -250,51 +284,631 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {},
|
||||
"properties": {
|
||||
"showName": false
|
||||
},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "graph",
|
||||
"graph": {
|
||||
"blocks": [],
|
||||
"connections": [],
|
||||
"annotations": []
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": "initial out = initial;\nout = der(in);",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "parameter-331e38be",
|
||||
"name": "initial",
|
||||
"type": "real",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "9011e8f6-940a-40ca-8bdf-773f0ed1c9f3",
|
||||
"name": "g_P",
|
||||
"position": {
|
||||
"x": -64.0,
|
||||
"y": -288.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-df34ce84",
|
||||
"name": "in",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 64.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-fe9e6486",
|
||||
"name": "out",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 88.0,
|
||||
"y": 40.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"showName": true
|
||||
},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": "out = k*in;",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "parameter-61a43a86",
|
||||
"name": "k",
|
||||
"type": "real",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "1995074c-4580-4ca3-8691-6923aedc5833",
|
||||
"name": "g_I",
|
||||
"position": {
|
||||
"x": -128.0,
|
||||
"y": -32.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-df34ce84",
|
||||
"name": "in",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 64.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-fe9e6486",
|
||||
"name": "out",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 88.0,
|
||||
"y": 40.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"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
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"showName": true
|
||||
},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": "out = k*in;",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "parameter-61a43a86",
|
||||
"name": "k",
|
||||
"type": "real",
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c7440f4d-f206-4c27-9fd2-5722950f207f",
|
||||
"name": "add0",
|
||||
"position": {
|
||||
"x": 128.0,
|
||||
"y": -160.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-c995845c",
|
||||
"name": "in",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 64.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": true
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-a2f0dea6",
|
||||
"name": "out",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 80.0,
|
||||
"y": 48.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"shape": "rectangle",
|
||||
"fill": "#f4f4f4",
|
||||
"border": "#303030",
|
||||
"text": "Text",
|
||||
"size": {
|
||||
"width": 128.0,
|
||||
"height": 128.0
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"fill": "#f4f4f4",
|
||||
"height": 48.0,
|
||||
"lineStyle": "solid",
|
||||
"lineWidth": 1.5,
|
||||
"stroke": "#303030",
|
||||
"type": "circle",
|
||||
"width": 48.0,
|
||||
"x": 40.0,
|
||||
"y": 40.0
|
||||
},
|
||||
{
|
||||
"color": "#00007f",
|
||||
"fill": "none",
|
||||
"fontSize": 18.0,
|
||||
"height": 32.0,
|
||||
"lineStyle": "none",
|
||||
"lineWidth": 1.5,
|
||||
"stroke": "#00007f",
|
||||
"text": "+",
|
||||
"type": "text",
|
||||
"width": 32.0,
|
||||
"x": 48.0,
|
||||
"y": 48.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"showName": false
|
||||
},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": "out = sum(in[i] for i in 1:in.N);",
|
||||
"parameters": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "48af4110-839a-45f7-a105-7e7dbc51676b",
|
||||
"name": "integrate0",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": -32.0
|
||||
},
|
||||
"rotation": 0.0,
|
||||
"interface": {
|
||||
"inputs": [
|
||||
{
|
||||
"id": "port-12903207",
|
||||
"name": "in",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 64.0,
|
||||
"y": 64.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": "port-b3370b1a",
|
||||
"name": "out",
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0
|
||||
},
|
||||
"properties": {
|
||||
"iconPosition": {
|
||||
"x": 88.0,
|
||||
"y": 40.0
|
||||
}
|
||||
},
|
||||
"type": "signal",
|
||||
"multipleConnections": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"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
|
||||
},
|
||||
{
|
||||
"color": "#00007f",
|
||||
"fill": "#ffffff",
|
||||
"fontSize": 18.0,
|
||||
"height": 48.0,
|
||||
"lineStyle": "solid",
|
||||
"lineWidth": 1.5,
|
||||
"stroke": "#00007f",
|
||||
"text": "dt",
|
||||
"type": "text",
|
||||
"width": 28.0,
|
||||
"x": 64.0,
|
||||
"y": 40.0
|
||||
},
|
||||
{
|
||||
"color": "#00007f",
|
||||
"fill": "none",
|
||||
"fontSize": 32.0,
|
||||
"height": 54.0,
|
||||
"lineStyle": "none",
|
||||
"lineWidth": 1.5,
|
||||
"stroke": "#00007f",
|
||||
"text": "\u222b",
|
||||
"type": "text",
|
||||
"width": 28.0,
|
||||
"x": 40.0,
|
||||
"y": 32.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"showName": false
|
||||
},
|
||||
"library": {
|
||||
"showSubtree": true
|
||||
},
|
||||
"implementation": {
|
||||
"kind": "text",
|
||||
"source": {
|
||||
"equations": "initial out = initial;\nder(out) = in;",
|
||||
"parameters": [
|
||||
{
|
||||
"id": "parameter-6bdc1c76",
|
||||
"name": "initial",
|
||||
"type": "real",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"id": "87490296-e353-43ea-8b59-55329950fdce",
|
||||
"id": "f89a8edb-b662-4a55-aa00-6d681b0bf6ae",
|
||||
"source": {
|
||||
"block": "af73487a-4a2d-4908-b919-d0a2eaa75ce2",
|
||||
"port": "port-de109124"
|
||||
"block": "9011e8f6-940a-40ca-8bdf-773f0ed1c9f3",
|
||||
"port": "port-fe9e6486"
|
||||
},
|
||||
"target": {
|
||||
"block": "b89a77a1-d773-4ee3-8c3f-74377c29b4b2",
|
||||
"port": "port-4f732b3e"
|
||||
"block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
|
||||
"port": "port-c995845c"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {
|
||||
"waypoints": [
|
||||
{
|
||||
"x": -64.0,
|
||||
"y": -192.0
|
||||
"x": 192.0,
|
||||
"y": -224.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": -64.0,
|
||||
"y": -256.0
|
||||
"id": "c1a2f8e2-b98f-45ee-9ef6-fad8cd42b71b",
|
||||
"source": {
|
||||
"block": "478e7ff8-baa1-467f-8707-e375b99395f6",
|
||||
"port": "port-4eeea4e7"
|
||||
},
|
||||
"target": {
|
||||
"block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
|
||||
"port": "port-c995845c"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {
|
||||
"waypoints": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 64.0,
|
||||
"y": -256.0
|
||||
"id": "e889f7bd-c684-4e59-8e9d-1693bd2ba87f",
|
||||
"source": {
|
||||
"block": "cbfb5d1f-38cc-412e-b8e3-53aa82e46eb8",
|
||||
"port": "port-fe9e6486"
|
||||
},
|
||||
"target": {
|
||||
"block": "478e7ff8-baa1-467f-8707-e375b99395f6",
|
||||
"port": "port-1cbabc8f"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {
|
||||
"waypoints": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "1cebb01c-7da4-48c7-b7ea-5585f64cd524",
|
||||
"source": {
|
||||
"block": "1995074c-4580-4ca3-8691-6923aedc5833",
|
||||
"port": "port-fe9e6486"
|
||||
},
|
||||
"target": {
|
||||
"block": "48af4110-839a-45f7-a105-7e7dbc51676b",
|
||||
"port": "port-12903207"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {
|
||||
"waypoints": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "6d16540e-d902-4ebf-a6a1-dcdda0cc8458",
|
||||
"source": {
|
||||
"block": "48af4110-839a-45f7-a105-7e7dbc51676b",
|
||||
"port": "port-b3370b1a"
|
||||
},
|
||||
"target": {
|
||||
"block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
|
||||
"port": "port-c995845c"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {
|
||||
"waypoints": [
|
||||
{
|
||||
"x": 192.0,
|
||||
"y": 32.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "206f1501-4509-4ba7-a28e-156e35de51ea",
|
||||
"source": {
|
||||
"block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
|
||||
"port": "port-a2f0dea6"
|
||||
},
|
||||
"target": {
|
||||
"interface": "port-c75d0f16"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {
|
||||
"waypoints": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5d234bec-8754-46e8-b9e8-1c802bb9f9ba",
|
||||
"source": {
|
||||
"interface": "port-1342716d"
|
||||
},
|
||||
"target": {
|
||||
"junction": "7a552e5e-b15d-46ca-a891-37e5129f88d1"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {
|
||||
"waypoints": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "33ba6f27-6f3a-4290-baac-b3cddcaa6b50",
|
||||
"source": {
|
||||
"junction": "7a552e5e-b15d-46ca-a891-37e5129f88d1"
|
||||
},
|
||||
"target": {
|
||||
"block": "cbfb5d1f-38cc-412e-b8e3-53aa82e46eb8",
|
||||
"port": "port-df34ce84"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {
|
||||
"waypoints": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ee4a3409-fc46-485c-b475-b456dddb93b5",
|
||||
"source": {
|
||||
"junction": "7a552e5e-b15d-46ca-a891-37e5129f88d1"
|
||||
},
|
||||
"target": {
|
||||
"block": "9011e8f6-940a-40ca-8bdf-773f0ed1c9f3",
|
||||
"port": "port-df34ce84"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {
|
||||
"waypoints": [
|
||||
{
|
||||
"x": -192.0,
|
||||
"y": -224.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8d1a5f3c-b706-4671-873a-2121f55f748b",
|
||||
"source": {
|
||||
"junction": "7a552e5e-b15d-46ca-a891-37e5129f88d1"
|
||||
},
|
||||
"target": {
|
||||
"block": "1995074c-4580-4ca3-8691-6923aedc5833",
|
||||
"port": "port-df34ce84"
|
||||
},
|
||||
"name": "",
|
||||
"properties": {
|
||||
"waypoints": [
|
||||
{
|
||||
"x": -192.0,
|
||||
"y": 32.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"annotations": []
|
||||
"annotations": [],
|
||||
"junctions": [
|
||||
{
|
||||
"id": "7a552e5e-b15d-46ca-a891-37e5129f88d1",
|
||||
"position": {
|
||||
"x": -192.0,
|
||||
"y": -96.0
|
||||
},
|
||||
"type": "signal"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user